diff options
Diffstat (limited to 'sonar-server/src')
286 files changed, 33294 insertions, 13380 deletions
diff --git a/sonar-server/src/dev/derby/conf/sonar.properties b/sonar-server/src/dev/derby/conf/sonar.properties index 033e2f27587..978f79ef7eb 100644 --- a/sonar-server/src/dev/derby/conf/sonar.properties +++ b/sonar-server/src/dev/derby/conf/sonar.properties @@ -1,8 +1,8 @@ # Derby -sonar.jdbc.url: jdbc:derby://localhost:1523/sonar;create=true +sonar.jdbc.url: jdbc:derby://localhost:1527/sonar;create=true sonar.jdbc.driverClassName: org.apache.derby.jdbc.ClientDriver sonar.jdbc.defaultTransactionIsolation: 1 -sonar.jdbc.username: sonar +sonar.jdbc.username: sonar sonar.jdbc.password: sonar sonar.jdbc.maxActive: 30 sonar.jdbc.maxIdle: 10 @@ -13,5 +13,5 @@ sonar.jdbc.timeBetweenEvictionRunsMillis: 30000 sonar.runtime.mode: development -sonar.derby.drda.portNumber: 1523 +sonar.derby.drda.portNumber: 1527 sonar.derby.drda.host: localhost
\ No newline at end of file diff --git a/sonar-server/src/dev/oracle/sonar.properties b/sonar-server/src/dev/oracle/sonar.properties index 8ebce5e28e5..b5125aa551f 100644 --- a/sonar-server/src/dev/oracle/sonar.properties +++ b/sonar-server/src/dev/oracle/sonar.properties @@ -1,7 +1,7 @@ # Oracle sonar.jdbc.url: jdbc:oracle:thin:@192.168.1.99/XE sonar.jdbc.driverClassName: oracle.jdbc.driver.OracleDriver -sonar.jdbc.username: sonar +sonar.jdbc.username: sonar sonar.jdbc.password: sonar sonar.jdbc.maxActive: 30 sonar.jdbc.maxIdle: 10 diff --git a/sonar-server/src/main/java/org/sonar/server/database/JndiDatabaseConnector.java b/sonar-server/src/main/java/org/sonar/server/database/JndiDatabaseConnector.java index fa694867bd7..abcb18f9a4c 100644 --- a/sonar-server/src/main/java/org/sonar/server/database/JndiDatabaseConnector.java +++ b/sonar-server/src/main/java/org/sonar/server/database/JndiDatabaseConnector.java @@ -21,17 +21,12 @@ package org.sonar.server.database; import org.apache.commons.configuration.Configuration; import org.apache.commons.dbcp.BasicDataSourceFactory; -import org.apache.commons.lang.StringUtils; import org.hibernate.cfg.Environment; -import org.sonar.api.database.DatabaseProperties; import org.sonar.api.utils.Logs; import org.sonar.api.utils.SonarException; import org.sonar.jpa.entity.SchemaMigration; import org.sonar.jpa.session.AbstractDatabaseConnector; -import javax.naming.Context; -import javax.naming.InitialContext; -import javax.naming.NamingException; import javax.sql.DataSource; import java.sql.Connection; import java.sql.SQLException; @@ -40,13 +35,10 @@ import java.util.Properties; public class JndiDatabaseConnector extends AbstractDatabaseConnector { - static final String JNDI_ENV_CONTEXT = "java:comp/env"; private DataSource datasource = null; - private String jndiKey; public JndiDatabaseConnector(Configuration configuration) { super(configuration, false); - jndiKey = getConfiguration().getString(DatabaseProperties.PROP_JNDI_NAME); } @Override @@ -62,11 +54,7 @@ public class JndiDatabaseConnector extends AbstractDatabaseConnector { @Override public void start() { if (!isStarted()) { - if (StringUtils.isNotBlank(jndiKey)) { - loadJndiDatasource(); - } else { - createDatasource(); - } + createDatasource(); } if (!super.isOperational()) { super.start(); @@ -80,41 +68,20 @@ public class JndiDatabaseConnector extends AbstractDatabaseConnector { } - private void loadJndiDatasource() { - Context ctx; - try { - ctx = new InitialContext(); - - } catch (NamingException e) { - throw new SonarException("Can not instantiate JNDI context", e); - } - - try { - Context envCtx = (Context) ctx.lookup(JNDI_ENV_CONTEXT); - datasource = (DataSource) envCtx.lookup(jndiKey); - Logs.INFO.info("JDBC datasource loaded from JNDI: " + jndiKey); - - } catch (NamingException e) { - throw new SonarException("JNDI context of JDBC datasource not found: " + jndiKey, e); - - } finally { - try { - ctx.close(); - } catch (NamingException e) { - } - } - } - private void createDatasource() { try { Logs.INFO.info("Creating JDBC datasource"); Properties properties = new Properties(); Configuration dsConfig = getConfiguration().subset("sonar.jdbc"); - for (Iterator<String> it = dsConfig.getKeys(); it.hasNext();) { + for (Iterator<String> it = dsConfig.getKeys(); it.hasNext(); ) { String key = it.next(); properties.setProperty(key, dsConfig.getString(key)); } + // This property is required by the Ruby Oracle enhanced adapter. + // It directly uses the Connection implementation provided by the Oracle driver + properties.setProperty("accessToUnderlyingConnectionAllowed", "true"); + datasource = BasicDataSourceFactory.createDataSource(properties); CustomHibernateConnectionProvider.datasource = datasource; } catch (Exception e) { @@ -135,11 +102,7 @@ public class JndiDatabaseConnector extends AbstractDatabaseConnector { @Override public void setupEntityManagerFactory(Properties factoryProps) { - if (StringUtils.isNotBlank(jndiKey)) { - factoryProps.put(Environment.DATASOURCE, JNDI_ENV_CONTEXT + "/" + jndiKey); - } else { - factoryProps.put(Environment.CONNECTION_PROVIDER, CustomHibernateConnectionProvider.class.getName()); - } + factoryProps.put(Environment.CONNECTION_PROVIDER, CustomHibernateConnectionProvider.class.getName()); } }
\ No newline at end of file diff --git a/sonar-server/src/main/java/org/sonar/server/mavendeployer/Artifact.java b/sonar-server/src/main/java/org/sonar/server/mavendeployer/Artifact.java index cf1793dd085..1c953d013c0 100644 --- a/sonar-server/src/main/java/org/sonar/server/mavendeployer/Artifact.java +++ b/sonar-server/src/main/java/org/sonar/server/mavendeployer/Artifact.java @@ -36,26 +36,18 @@ public class Artifact { private String groupId; private String artifactId; protected String version; - private Artifact[] dependencies; protected File jar; private String packaging; - public Artifact(String groupId, String artifactId, String version, String packaging, File jar, Artifact... deps) { + public Artifact(String groupId, String artifactId, String version, String packaging, File jar) { this.artifactId = artifactId; this.groupId = groupId; this.version = version; - this.dependencies = deps; this.jar = jar; this.packaging = packaging; } - public void deployTo(File rootDir, boolean deployDependencies) throws IOException { - if (deployDependencies && dependencies != null) { - for (Artifact dependency : dependencies) { - dependency.deployTo(rootDir, true); - } - } - + public void deployTo(File rootDir) throws IOException { File dir = createDir(rootDir); savePom(dir); saveMetadata(dir); @@ -85,18 +77,6 @@ public class Artifact { return version; } - public Artifact[] getDependencies() { - return dependencies; - } - - public File getJar() { - return jar; - } - - public String getPackaging() { - return packaging; - } - private void saveJar(File dir) throws IOException { if (jar != null) { copyTo(dir); @@ -125,23 +105,13 @@ public class Artifact { } public String getPom() throws IOException { - return transformFromTemplatePath(getTemplatePath(), getDependenciesAsString()); + return transformFromTemplatePath(getTemplatePath()); } protected String getTemplatePath() { return "/org/sonar/server/mavendeployer/pom.template"; } - private String getDependenciesAsString() throws IOException { - StringBuilder sb = new StringBuilder(); - if (dependencies != null) { - for (Artifact dependency : dependencies) { - sb.append(dependency.getXmlDefinition()); - } - } - return sb.toString(); - } - @Override public boolean equals(Object o) { if (this == o) { @@ -179,11 +149,7 @@ public class Artifact { FileUtils.writeStringToFile(metadataFile, getMetadata(), CharEncoding.UTF_8); } - protected String transformFromTemplatePath(String templatePath) throws IOException { - return transformFromTemplatePath(templatePath, ""); - } - - protected final String transformFromTemplatePath(String templatePath, String depsXml) throws IOException { + protected final String transformFromTemplatePath(String templatePath) throws IOException { InputStream template = this.getClass().getResourceAsStream(templatePath); try { String content = IOUtils.toString(template); @@ -192,7 +158,6 @@ public class Artifact { content = StringUtils.replace(content, "$version", version); content = StringUtils.replace(content, "$timestamp", version); content = StringUtils.replace(content, "$packaging", packaging); - content = StringUtils.replace(content, "$dependencies", StringUtils.defaultString(depsXml, "")); return content; } finally { diff --git a/sonar-server/src/main/java/org/sonar/server/mavendeployer/MavenRepository.java b/sonar-server/src/main/java/org/sonar/server/mavendeployer/MavenRepository.java index 8dbd3e57d59..22455c1a057 100644 --- a/sonar-server/src/main/java/org/sonar/server/mavendeployer/MavenRepository.java +++ b/sonar-server/src/main/java/org/sonar/server/mavendeployer/MavenRepository.java @@ -52,7 +52,7 @@ public class MavenRepository { public void start() { try { Artifact maven2Plugin = Mojo.createMaven2Plugin(serverId, installation.getMaven2Plugin()); - maven2Plugin.deployTo(rootDir, false); + maven2Plugin.deployTo(rootDir); } catch (IOException e) { throw new RuntimeException(e); diff --git a/sonar-server/src/main/java/org/sonar/server/mavendeployer/Mojo.java b/sonar-server/src/main/java/org/sonar/server/mavendeployer/Mojo.java index bd90adea321..615dbf4ae52 100644 --- a/sonar-server/src/main/java/org/sonar/server/mavendeployer/Mojo.java +++ b/sonar-server/src/main/java/org/sonar/server/mavendeployer/Mojo.java @@ -28,16 +28,12 @@ import java.io.IOException; public final class Mojo extends Artifact { - private Mojo(String artifactId, String version, File jar, Artifact... deps) { - super(BASE_GROUP_ID, artifactId, version, "maven-plugin", jar, deps); + private Mojo(String artifactId, String version, File jar) { + super(BASE_GROUP_ID, artifactId, version, "maven-plugin", jar); } - public static Mojo createMaven2Plugin(String version, File jar, Artifact... deps) { - return new Mojo("sonar-core-maven-plugin", version, jar, deps); - } - - public static Mojo createMaven3Plugin(String version, File jar, Artifact... deps) { - return new Mojo("sonar-core-maven3-plugin", version, jar, deps); + public static Mojo createMaven2Plugin(String version, File jar) { + return new Mojo("sonar-core-maven-plugin", version, jar); } @Override diff --git a/sonar-server/src/main/java/org/sonar/server/platform/DefaultServerFileSystem.java b/sonar-server/src/main/java/org/sonar/server/platform/DefaultServerFileSystem.java index aef353f6587..919217b72e0 100644 --- a/sonar-server/src/main/java/org/sonar/server/platform/DefaultServerFileSystem.java +++ b/sonar-server/src/main/java/org/sonar/server/platform/DefaultServerFileSystem.java @@ -155,7 +155,7 @@ public class DefaultServerFileSystem implements ServerFileSystem { return new File(getHomeDir(), "extensions/rules"); } - public File getPluginsIndex() { + public File getPluginIndex() { return new File(getDeployDir(), "plugins/index.txt"); } diff --git a/sonar-server/src/main/java/org/sonar/server/platform/Platform.java b/sonar-server/src/main/java/org/sonar/server/platform/Platform.java index 2d8ef632fa9..c733a991588 100644 --- a/sonar-server/src/main/java/org/sonar/server/platform/Platform.java +++ b/sonar-server/src/main/java/org/sonar/server/platform/Platform.java @@ -137,14 +137,15 @@ public final class Platform { private void startCoreComponents() { coreContainer = rootContainer.makeChildContainer(); coreContainer.as(Characteristics.CACHE).addComponent(PluginDeployer.class); - coreContainer.as(Characteristics.CACHE).addComponent(ServerPluginRepository.class); - coreContainer.as(Characteristics.CACHE).addComponent(ServerImpl.class); + coreContainer.as(Characteristics.CACHE).addComponent(DefaultServerPluginRepository.class); coreContainer.as(Characteristics.CACHE).addComponent(DefaultServerFileSystem.class); coreContainer.as(Characteristics.CACHE).addComponent(ThreadLocalDatabaseSessionFactory.class); coreContainer.as(Characteristics.CACHE).addComponent(HttpDownloader.class); coreContainer.as(Characteristics.CACHE).addComponent(UpdateCenterClient.class); coreContainer.as(Characteristics.CACHE).addComponent(UpdateCenterMatrixFactory.class); coreContainer.as(Characteristics.CACHE).addComponent(PluginDownloader.class); + coreContainer.as(Characteristics.CACHE).addComponent(ServerIdGenerator.class); + coreContainer.as(Characteristics.CACHE).addComponent(ServerImpl.class); coreContainer.as(Characteristics.NO_CACHE).addComponent(FilterExecutor.class); coreContainer.as(Characteristics.NO_CACHE).addAdapter(new DatabaseSessionProvider()); coreContainer.start(); @@ -159,7 +160,7 @@ public final class Platform { private void startServiceComponents() { servicesContainer = coreContainer.makeChildContainer(); - ServerPluginRepository pluginRepository = servicesContainer.getComponent(ServerPluginRepository.class); + DefaultServerPluginRepository pluginRepository = servicesContainer.getComponent(DefaultServerPluginRepository.class); pluginRepository.registerExtensions(servicesContainer); servicesContainer.as(Characteristics.CACHE).addComponent(DefaultModelFinder.class); // depends on plugins @@ -216,6 +217,7 @@ public final class Platform { startupContainer.as(Characteristics.CACHE).addComponent(ServerMetadataPersister.class); startupContainer.as(Characteristics.CACHE).addComponent(RegisterQualityModels.class); startupContainer.as(Characteristics.CACHE).addComponent(DeleteDeprecatedMeasures.class); + startupContainer.as(Characteristics.CACHE).addComponent(GeneratePluginIndex.class); startupContainer.start(); startupContainer.getComponent(ServerLifecycleNotifier.class).notifyStart(); diff --git a/sonar-server/src/main/java/org/sonar/server/platform/ServerIdGenerator.java b/sonar-server/src/main/java/org/sonar/server/platform/ServerIdGenerator.java new file mode 100644 index 00000000000..7d09dd95c99 --- /dev/null +++ b/sonar-server/src/main/java/org/sonar/server/platform/ServerIdGenerator.java @@ -0,0 +1,119 @@ +/* + * Sonar, open source software quality management tool. + * Copyright (C) 2008-2011 SonarSource + * mailto:contact AT sonarsource DOT com + * + * Sonar is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * Sonar is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with Sonar; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package org.sonar.server.platform; + +import com.google.common.collect.Lists; +import org.apache.commons.codec.digest.DigestUtils; +import org.apache.commons.lang.StringUtils; +import org.slf4j.LoggerFactory; + +import java.io.UnsupportedEncodingException; +import java.net.InetAddress; +import java.net.NetworkInterface; +import java.net.SocketException; +import java.net.UnknownHostException; +import java.util.Enumeration; +import java.util.List; + +/** + * @since 2.11 + */ +public class ServerIdGenerator { + + /** + * Increment this version each time the algorithm is changed. Do not exceed 9. + */ + static final String VERSION = "1"; + + static final int CHECKSUM_SIZE = 14; + + private final boolean acceptPrivateAddress; + + public ServerIdGenerator() { + this(false); + } + + ServerIdGenerator(boolean acceptPrivateAddress) { + this.acceptPrivateAddress = acceptPrivateAddress; + } + + public String generate(String organisation, String ipAddress) { + String id = null; + if (StringUtils.isNotBlank(organisation) && StringUtils.isNotBlank(ipAddress)) { + InetAddress inetAddress = toValidAddress(ipAddress); + if (inetAddress != null) { + id = toId(organisation, inetAddress); + } + } + return id; + } + + boolean isFixed(InetAddress address) { + // Loopback addresses are in the range 127/8. + // Link local addresses are in the range 169.254/16 (IPv4) or fe80::/10 (IPv6). They are "autoconfiguration" addresses. + // They can assigned pseudorandomly, so they don't guarantee to be the same between two server startups. + return acceptPrivateAddress || (!address.isLoopbackAddress() && !address.isLinkLocalAddress()); + } + + String toId(String organisation, InetAddress address) { + String id = new StringBuilder().append(organisation).append("-").append(address.getHostAddress()).toString(); + try { + return VERSION + DigestUtils.shaHex(id.getBytes("UTF-8")).substring(0, CHECKSUM_SIZE); + + } catch (UnsupportedEncodingException e) { + throw new IllegalArgumentException("Organisation is not UTF-8 encoded: " + organisation, e); + } + } + + public InetAddress toValidAddress(String ipAddress) { + if (StringUtils.isNotBlank(ipAddress)) { + List<InetAddress> validAddresses = getAvailableAddresses(); + try { + InetAddress address = InetAddress.getByName(ipAddress); + if (validAddresses.contains(address)) { + return address; + } + } catch (UnknownHostException e) { + // ignore, not valid property + } + } + return null; + } + + public List<InetAddress> getAvailableAddresses() { + List<InetAddress> result = Lists.newArrayList(); + try { + Enumeration<NetworkInterface> networkInterfaces = NetworkInterface.getNetworkInterfaces(); + while (networkInterfaces.hasMoreElements()) { + NetworkInterface networkInterface = networkInterfaces.nextElement(); + Enumeration<InetAddress> addresses = networkInterface.getInetAddresses(); + while (addresses.hasMoreElements()) { + InetAddress ownedAddress = addresses.nextElement(); + if (isFixed(ownedAddress)) { + result.add(ownedAddress); + } + } + } + } catch (SocketException e) { + LoggerFactory.getLogger(ServerIdGenerator.class).error("Fail to browse network interfaces", e); + } + return result; + } +} diff --git a/sonar-server/src/main/java/org/sonar/server/platform/ServerImpl.java b/sonar-server/src/main/java/org/sonar/server/platform/ServerImpl.java index 5267fbcc8eb..abd659627fc 100644 --- a/sonar-server/src/main/java/org/sonar/server/platform/ServerImpl.java +++ b/sonar-server/src/main/java/org/sonar/server/platform/ServerImpl.java @@ -21,7 +21,11 @@ package org.sonar.server.platform; import org.apache.commons.io.IOUtils; import org.apache.commons.lang.StringUtils; +import org.sonar.api.CoreProperties; +import org.sonar.api.database.DatabaseSession; +import org.sonar.api.database.configuration.Property; import org.sonar.api.platform.Server; +import org.sonar.jpa.session.DatabaseSessionFactory; import java.io.IOException; import java.io.InputStream; @@ -31,28 +35,41 @@ import java.util.Properties; public final class ServerImpl extends Server { - private final String id; - private final String version; + private String id; + private String version; private final Date startedAt; - public ServerImpl() { + /** + * This component can't use Configuration because of startup sequence. It must be started before plugins. + */ + private DatabaseSessionFactory dbSessionFactory; + + public ServerImpl(DatabaseSessionFactory dbSessionFactory) { + this(dbSessionFactory, new Date()); + } + + ServerImpl(DatabaseSessionFactory dbSessionFactory, Date startedAt) { + this.dbSessionFactory = dbSessionFactory; + this.startedAt = startedAt; + } + + public void start() { try { - this.startedAt = new Date(); - this.id = new SimpleDateFormat("yyyyMMddHHmmss").format(startedAt); - this.version = loadVersionFromManifest("/META-INF/maven/org.codehaus.sonar/sonar-plugin-api/pom.properties"); - if (StringUtils.isBlank(this.version)) { + id = new SimpleDateFormat("yyyyMMddHHmmss").format(startedAt); + version = loadVersionFromManifest("/META-INF/maven/org.codehaus.sonar/sonar-plugin-api/pom.properties"); + if (StringUtils.isBlank(version)) { throw new ServerStartException("Unknown Sonar version"); } } catch (IOException e) { - throw new ServerStartException("Can not load Sonar metadata", e); + throw new ServerStartException("Can not load metadata", e); } } - public ServerImpl(String id, String version, Date startedAt) { - this.id = id; - this.version = version; - this.startedAt = startedAt; + public String getPermanentServerId() { + DatabaseSession session = dbSessionFactory.getSession(); + Property serverId = session.getSingleResult(Property.class, "key", CoreProperties.PERMANENT_SERVER_ID); + return (serverId!= null ? serverId.getValue() : null); } public String getId() { diff --git a/sonar-server/src/main/java/org/sonar/server/plugins/ServerPluginRepository.java b/sonar-server/src/main/java/org/sonar/server/plugins/DefaultServerPluginRepository.java index b937d6d30c0..73defaa30d4 100644 --- a/sonar-server/src/main/java/org/sonar/server/plugins/ServerPluginRepository.java +++ b/sonar-server/src/main/java/org/sonar/server/plugins/DefaultServerPluginRepository.java @@ -19,28 +19,31 @@ */ package org.sonar.server.plugins; +import com.google.common.collect.Sets; import org.picocontainer.Characteristics; import org.picocontainer.MutablePicoContainer; import org.slf4j.LoggerFactory; import org.sonar.api.*; import org.sonar.api.platform.PluginMetadata; -import org.sonar.api.platform.PluginRepository; +import org.sonar.api.platform.ServerPluginRepository; import org.sonar.core.plugins.PluginClassloaders; import java.util.Collection; import java.util.List; import java.util.Map; +import java.util.Set; /** * @since 2.2 */ -public class ServerPluginRepository implements PluginRepository { +public class DefaultServerPluginRepository implements ServerPluginRepository { private PluginClassloaders classloaders; private PluginDeployer deployer; private Map<String, Plugin> pluginsByKey; + private Set<String> disabledPlugins = Sets.newHashSet(); - public ServerPluginRepository(PluginDeployer deployer) { + public DefaultServerPluginRepository(PluginDeployer deployer) { this.classloaders = new PluginClassloaders(getClass().getClassLoader()); this.deployer = deployer; } @@ -56,6 +59,19 @@ public class ServerPluginRepository implements PluginRepository { } } + public void disable(String pluginKey) { + disabledPlugins.add(pluginKey); + for (PluginMetadata metadata : getMetadata()) { + if (pluginKey.equals(metadata.getBasePlugin())) { + disable(metadata.getKey()); + } + } + } + + public boolean isDisabled(String pluginKey) { + return disabledPlugins.contains(pluginKey); + } + public Collection<Plugin> getPlugins() { return pluginsByKey.values(); } @@ -109,7 +125,7 @@ public class ServerPluginRepository implements PluginRepository { for (Plugin plugin : plugins) { container.as(Characteristics.CACHE).addComponent(plugin); for (Object extension : plugin.getExtensions()) { - installExtension(container, extension); + installExtension(container, extension, true); } } installExtensionProviders(container); @@ -119,19 +135,25 @@ public class ServerPluginRepository implements PluginRepository { List<ExtensionProvider> providers = container.getComponents(ExtensionProvider.class); for (ExtensionProvider provider : providers) { Object obj = provider.provide(); - if (obj instanceof Iterable) { - for (Object extension : (Iterable) obj) { - installExtension(container, extension); + if (obj != null) { + if (obj instanceof Iterable) { + for (Object extension : (Iterable) obj) { + installExtension(container, extension, false); + } + } else { + installExtension(container, obj, false); } - } else { - installExtension(container, obj); } } } - void installExtension(MutablePicoContainer container, Object extension) { + void installExtension(MutablePicoContainer container, Object extension, boolean acceptProvider) { if (isType(extension, ServerExtension.class)) { - container.as(Characteristics.CACHE).addComponent(getExtensionKey(extension), extension); + if (!acceptProvider && (isType(extension, ExtensionProvider.class) || extension instanceof ExtensionProvider)) { + LoggerFactory.getLogger(getClass()).error("ExtensionProvider can not include providers itself: " + extension); + } else { + container.as(Characteristics.CACHE).addComponent(getExtensionKey(extension), extension); + } } } diff --git a/sonar-server/src/main/java/org/sonar/server/plugins/PluginDeployer.java b/sonar-server/src/main/java/org/sonar/server/plugins/PluginDeployer.java index a5c9281dbd3..3f957be9170 100644 --- a/sonar-server/src/main/java/org/sonar/server/plugins/PluginDeployer.java +++ b/sonar-server/src/main/java/org/sonar/server/plugins/PluginDeployer.java @@ -22,8 +22,6 @@ package org.sonar.server.plugins; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import org.apache.commons.io.FileUtils; -import org.apache.commons.io.IOUtils; -import org.apache.commons.lang.CharUtils; import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -34,12 +32,10 @@ import org.sonar.api.utils.SonarException; import org.sonar.api.utils.TimeProfiler; import org.sonar.core.plugins.DefaultPluginMetadata; import org.sonar.core.plugins.PluginFileExtractor; -import org.sonar.core.plugins.RemotePlugin; import org.sonar.server.platform.DefaultServerFileSystem; import org.sonar.server.platform.ServerStartException; import java.io.File; -import java.io.FileWriter; import java.io.IOException; import java.util.Collection; import java.util.List; @@ -73,7 +69,6 @@ public class PluginDeployer implements ServerComponent { deployPlugins(); - generateIndexFile(); profiler.stop(); } @@ -148,24 +143,6 @@ public class PluginDeployer implements ServerComponent { } } - - private void generateIndexFile() throws IOException { - File indexFile = fileSystem.getPluginsIndex(); - FileUtils.forceMkdir(indexFile.getParentFile()); - FileWriter writer = new FileWriter(indexFile, false); - try { - for (PluginMetadata metadata : pluginByKeys.values()) { - writer.append(RemotePlugin.create((DefaultPluginMetadata)metadata).marshal()); - writer.append(CharUtils.LF); - } - writer.flush(); - - } finally { - IOUtils.closeQuietly(writer); - } - } - - public void uninstall(String pluginKey) { PluginMetadata metadata = pluginByKeys.get(pluginKey); if (metadata != null && !metadata.isCore()) { diff --git a/sonar-server/src/main/java/org/sonar/server/plugins/StaticResourcesServlet.java b/sonar-server/src/main/java/org/sonar/server/plugins/StaticResourcesServlet.java index 1ba14e3c38c..1dca1b380ea 100644 --- a/sonar-server/src/main/java/org/sonar/server/plugins/StaticResourcesServlet.java +++ b/sonar-server/src/main/java/org/sonar/server/plugins/StaticResourcesServlet.java @@ -44,7 +44,7 @@ public class StaticResourcesServlet extends HttpServlet { String pluginKey = getPluginKey(request); String resource = getResourcePath(request); - ServerPluginRepository pluginRepository = Platform.getInstance().getContainer().getComponent(ServerPluginRepository.class); + DefaultServerPluginRepository pluginRepository = Platform.getInstance().getContainer().getComponent(DefaultServerPluginRepository.class); ClassLoader classLoader = pluginRepository.getClassloader(pluginKey); if (classLoader == null) { LOG.error("Plugin not found: " + pluginKey); diff --git a/sonar-server/src/main/java/org/sonar/server/startup/GeneratePluginIndex.java b/sonar-server/src/main/java/org/sonar/server/startup/GeneratePluginIndex.java new file mode 100644 index 00000000000..41317a5f5d5 --- /dev/null +++ b/sonar-server/src/main/java/org/sonar/server/startup/GeneratePluginIndex.java @@ -0,0 +1,68 @@ +/* + * Sonar, open source software quality management tool. + * Copyright (C) 2008-2011 SonarSource + * mailto:contact AT sonarsource DOT com + * + * Sonar is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * Sonar is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with Sonar; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package org.sonar.server.startup; + +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang.CharUtils; +import org.sonar.api.platform.PluginMetadata; +import org.sonar.core.plugins.DefaultPluginMetadata; +import org.sonar.core.plugins.RemotePlugin; +import org.sonar.server.platform.DefaultServerFileSystem; +import org.sonar.server.plugins.DefaultServerPluginRepository; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; + +/** + * @since 2.11 + */ +public final class GeneratePluginIndex { + + private DefaultServerFileSystem fileSystem; + private DefaultServerPluginRepository repository; + + public GeneratePluginIndex(DefaultServerFileSystem fileSystem, DefaultServerPluginRepository repository) { + this.fileSystem = fileSystem; + this.repository = repository; + } + + public void start() throws IOException { + writeIndex(fileSystem.getPluginIndex()); + } + + void writeIndex(File indexFile) throws IOException { + FileUtils.forceMkdir(indexFile.getParentFile()); + FileWriter writer = new FileWriter(indexFile, false); + try { + for (PluginMetadata metadata : repository.getMetadata()) { + if (!repository.isDisabled(metadata.getKey())) { + writer.append(RemotePlugin.create((DefaultPluginMetadata) metadata).marshal()); + writer.append(CharUtils.LF); + } + } + writer.flush(); + + } finally { + IOUtils.closeQuietly(writer); + } + } +} diff --git a/sonar-server/src/main/java/org/sonar/server/startup/ServerMetadataPersister.java b/sonar-server/src/main/java/org/sonar/server/startup/ServerMetadataPersister.java index 6a6e4092c52..cf6727d1769 100644 --- a/sonar-server/src/main/java/org/sonar/server/startup/ServerMetadataPersister.java +++ b/sonar-server/src/main/java/org/sonar/server/startup/ServerMetadataPersister.java @@ -39,19 +39,23 @@ public class ServerMetadataPersister { public void start() { setProperty(CoreProperties.SERVER_ID, server.getId()); setProperty(CoreProperties.SERVER_VERSION, server.getVersion()); - if (server.getStartedAt() != null) { - setProperty(CoreProperties.SERVER_STARTTIME, new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ").format(server.getStartedAt())); - } + setProperty(CoreProperties.SERVER_STARTTIME, new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ").format(server.getStartedAt())); session.commit(); } private void setProperty(String key, String value) { Property prop = session.getSingleResult(Property.class, "key", key); - if (prop == null) { - prop = new Property(key, value); - } else { - prop.setValue(value); + + if (value == null && prop != null) { + session.removeWithoutFlush(prop); + + } else if (value != null) { + if (prop == null) { + prop = new Property(key, value); + } else { + prop.setValue(value); + } + session.saveWithoutFlush(prop); } - session.save(prop); } -} +}
\ No newline at end of file diff --git a/sonar-server/src/main/java/org/sonar/server/ui/JRubyFacade.java b/sonar-server/src/main/java/org/sonar/server/ui/JRubyFacade.java index e72305ebacd..bb8d0d3b99b 100644 --- a/sonar-server/src/main/java/org/sonar/server/ui/JRubyFacade.java +++ b/sonar-server/src/main/java/org/sonar/server/ui/JRubyFacade.java @@ -44,13 +44,14 @@ import org.sonar.server.filters.FilterExecutor; import org.sonar.server.filters.FilterResult; import org.sonar.server.notifications.reviews.ReviewsNotificationManager; import org.sonar.server.platform.Platform; +import org.sonar.server.platform.ServerIdGenerator; import org.sonar.server.plugins.*; import org.sonar.server.rules.ProfilesConsole; import org.sonar.server.rules.RulesConsole; import org.sonar.updatecenter.common.Version; +import java.net.InetAddress; import java.sql.Connection; -import java.sql.SQLException; import java.util.Collection; import java.util.List; import java.util.Set; @@ -274,8 +275,21 @@ public final class JRubyFacade { return getContainer().getComponent(Configuration.class).getString(key, null); } - public Connection getConnection() throws SQLException { - return getContainer().getComponent(DatabaseConnector.class).getConnection(); + public List<InetAddress> getValidInetAddressesForServerId() { + return getContainer().getComponent(ServerIdGenerator.class).getAvailableAddresses(); + } + + public String generateServerId(String organisation, String ipAddress) { + return getContainer().getComponent(ServerIdGenerator.class).generate(organisation, ipAddress); + } + + public Connection getConnection() { + try { + return getContainer().getComponent(DatabaseConnector.class).getConnection(); + } catch (Exception e) { + /* activerecord does not correctly manage exceptions when connection can not be opened. */ + return null; + } } public Object getCoreComponentByClassname(String className) { @@ -296,7 +310,7 @@ public final class JRubyFacade { public Object getComponentByClassname(String pluginKey, String className) { Object component = null; PicoContainer container = getContainer(); - Class componentClass = container.getComponent(ServerPluginRepository.class).getClass(pluginKey, className); + Class componentClass = container.getComponent(DefaultServerPluginRepository.class).getClass(pluginKey, className); if (componentClass != null) { component = container.getComponent(componentClass); } diff --git a/sonar-server/src/main/resources/org/sonar/server/mavendeployer/sonar-core-maven-plugin.template b/sonar-server/src/main/resources/org/sonar/server/mavendeployer/sonar-core-maven-plugin.template index 1340868f93b..0c59a937c5d 100644 --- a/sonar-server/src/main/resources/org/sonar/server/mavendeployer/sonar-core-maven-plugin.template +++ b/sonar-server/src/main/resources/org/sonar/server/mavendeployer/sonar-core-maven-plugin.template @@ -10,26 +10,6 @@ </distributionManagement> <dependencies> <dependency> - <groupId>org.apache.maven.shared</groupId> - <artifactId>maven-dependency-tree</artifactId> - <version>1.2</version> - </dependency> - <dependency> - <groupId>org.codehaus.sonar</groupId> - <artifactId>sonar-batch</artifactId> - <version>${project.version}</version> - </dependency> - <dependency> - <groupId>ch.qos.logback</groupId> - <artifactId>logback-classic</artifactId> - <version>0.9.15</version> - </dependency> - <dependency> - <groupId>org.slf4j</groupId> - <artifactId>slf4j-api</artifactId> - <version>1.5.6</version> - </dependency> - <dependency> <groupId>org.apache.maven</groupId> <artifactId>maven-plugin-api</artifactId> <version>2.0.7</version> @@ -47,13 +27,5 @@ <version>2.0.7</version> <scope>provided</scope> </dependency> - <dependency> - <groupId>commons-lang</groupId> - <artifactId>commons-lang</artifactId> - <version>2.4</version> - </dependency> - - $dependencies - </dependencies> </project>
\ No newline at end of file diff --git a/sonar-server/src/main/webapp/WEB-INF/app/controllers/api/projects_controller.rb b/sonar-server/src/main/webapp/WEB-INF/app/controllers/api/projects_controller.rb index ad53b529d7f..fc93e0f224c 100644 --- a/sonar-server/src/main/webapp/WEB-INF/app/controllers/api/projects_controller.rb +++ b/sonar-server/src/main/webapp/WEB-INF/app/controllers/api/projects_controller.rb @@ -18,6 +18,8 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 # class Api::ProjectsController < Api::ApiController + + before_filter :admin_required, :only => [ :destroy ] # PARAMETERS # subprojects [true|false] : load sub-projects ? Default is false. Ignored if the parameter key is set. @@ -49,7 +51,21 @@ class Api::ProjectsController < Api::ApiController end end - + # + # DELETE /api/projects/<key> + # curl -X DELETE http://localhost:9000/api/projects/<key> -v -u admin:admin + # + def destroy + bad_request("Missing project key") unless params[:id].present? + + project = Project.by_key(params[:id]) + bad_request("Not valid project") unless project + access_denied unless is_admin?(project) + bad_request("Not valid project") unless project.project? + + Project.delete_project(project) + render_success("Project deleted") + end private diff --git a/sonar-server/src/main/webapp/WEB-INF/app/controllers/dashboard_controller.rb b/sonar-server/src/main/webapp/WEB-INF/app/controllers/dashboard_controller.rb index 40981b63a1c..f7245aefa82 100644 --- a/sonar-server/src/main/webapp/WEB-INF/app/controllers/dashboard_controller.rb +++ b/sonar-server/src/main/webapp/WEB-INF/app/controllers/dashboard_controller.rb @@ -95,7 +95,7 @@ class DashboardController < ApplicationController if dashboard.editable_by?(current_user) definition=java_facade.getWidget(params[:widget]) if definition - first_column_widgets=dashboard.widgets.select{|w| w.column_index==1} + first_column_widgets=dashboard.widgets.select{|w| w.column_index==1}.sort_by{|w| w.row_index} new_widget=dashboard.widgets.create(:widget_key => definition.getId(), :name => definition.getTitle(), :column_index => 1, diff --git a/sonar-server/src/main/webapp/WEB-INF/app/controllers/email_configuration_controller.rb b/sonar-server/src/main/webapp/WEB-INF/app/controllers/email_configuration_controller.rb index e8f89b50748..5e38fe875c7 100644 --- a/sonar-server/src/main/webapp/WEB-INF/app/controllers/email_configuration_controller.rb +++ b/sonar-server/src/main/webapp/WEB-INF/app/controllers/email_configuration_controller.rb @@ -30,6 +30,7 @@ class EmailConfigurationController < ApplicationController @smtp_password = Property.value(configuration::SMTP_PASSWORD, nil, configuration::SMTP_PASSWORD_DEFAULT) @email_from = Property.value(configuration::FROM, nil, configuration::FROM_DEFAULT) @email_prefix = Property.value(configuration::PREFIX, nil, configuration::PREFIX_DEFAULT) + params[:layout]='false' end def save diff --git a/sonar-server/src/main/webapp/WEB-INF/app/controllers/plugins/configuration_controller.rb b/sonar-server/src/main/webapp/WEB-INF/app/controllers/plugins/configuration_controller.rb index f5ea76a519d..5160ea28c8f 100644 --- a/sonar-server/src/main/webapp/WEB-INF/app/controllers/plugins/configuration_controller.rb +++ b/sonar-server/src/main/webapp/WEB-INF/app/controllers/plugins/configuration_controller.rb @@ -25,6 +25,8 @@ class Plugins::ConfigurationController < ApplicationController page_id=params[:page] @page_proxy=java_facade.getPage(page_id) + return redirect_to(home_path) unless @page_proxy + authorized=@page_proxy.getUserRoles().size==0 unless authorized @page_proxy.getUserRoles().each do |role| diff --git a/sonar-server/src/main/webapp/WEB-INF/app/controllers/plugins/home_controller.rb b/sonar-server/src/main/webapp/WEB-INF/app/controllers/plugins/home_controller.rb index 59ce20ce328..8f53a48b208 100644 --- a/sonar-server/src/main/webapp/WEB-INF/app/controllers/plugins/home_controller.rb +++ b/sonar-server/src/main/webapp/WEB-INF/app/controllers/plugins/home_controller.rb @@ -25,6 +25,7 @@ class Plugins::HomeController < ApplicationController page_id=params[:page] @page_proxy=java_facade.getPage(page_id) + return redirect_to(home_path) unless @page_proxy authorized=@page_proxy.getUserRoles().size==0 unless authorized @page_proxy.getUserRoles().each do |role| diff --git a/sonar-server/src/main/webapp/WEB-INF/app/controllers/plugins/resource_controller.rb b/sonar-server/src/main/webapp/WEB-INF/app/controllers/plugins/resource_controller.rb index 5db6657cebd..b8967a29821 100644 --- a/sonar-server/src/main/webapp/WEB-INF/app/controllers/plugins/resource_controller.rb +++ b/sonar-server/src/main/webapp/WEB-INF/app/controllers/plugins/resource_controller.rb @@ -31,7 +31,7 @@ class Plugins::ResourceController < ApplicationController page_id=params[:page] @page_proxy=java_facade.getPage(page_id) - return redirect_to home_url if @page_proxy.nil? + return redirect_to(home_path) unless @page_proxy authorized=@page_proxy.getUserRoles().size==0 unless authorized diff --git a/sonar-server/src/main/webapp/WEB-INF/app/controllers/profiles_controller.rb b/sonar-server/src/main/webapp/WEB-INF/app/controllers/profiles_controller.rb index 07083be7d88..2e27aeb9c4b 100644 --- a/sonar-server/src/main/webapp/WEB-INF/app/controllers/profiles_controller.rb +++ b/sonar-server/src/main/webapp/WEB-INF/app/controllers/profiles_controller.rb @@ -24,7 +24,7 @@ class ProfilesController < ApplicationController verify :method => :post, :only => ['create', 'delete', 'copy', 'set_as_default', 'restore', 'set_projects', 'rename', 'change_parent'], :redirect_to => { :action => 'index' } # the backup action is allow to non-admin users : see http://jira.codehaus.org/browse/SONAR-2039 - before_filter :admin_required, :except => [ 'index', 'show', 'projects', 'permalinks', 'export', 'backup', 'inheritance' ] + before_filter :admin_required, :only => ['create', 'delete', 'set_as_default', 'copy', 'restore', 'change_parent', 'set_projects', 'rename'] # # diff --git a/sonar-server/src/main/webapp/WEB-INF/app/controllers/project_controller.rb b/sonar-server/src/main/webapp/WEB-INF/app/controllers/project_controller.rb index 00be0035799..c9097ef560d 100644 --- a/sonar-server/src/main/webapp/WEB-INF/app/controllers/project_controller.rb +++ b/sonar-server/src/main/webapp/WEB-INF/app/controllers/project_controller.rb @@ -27,17 +27,62 @@ class ProjectController < ApplicationController redirect_to :overwrite_params => {:controller => :dashboard, :action => 'index'} end + def deletion + @project=Project.by_key(params[:id]) + return access_denied unless is_admin?(@project) + + @snapshot=@project.last_snapshot + if !@project.project? + redirect_to :action => 'index', :id => params[:id] + end + end + def delete if params[:id] @project = Project.by_key(params[:id]) - if @project && is_admin?(@project) - Snapshot.update_all(['islast=?', false], ['(root_project_id=? OR project_id=?) AND islast=?', @project.id, @project.id, true]) - Project.delete_all(['id=? OR root_id=? or copy_resource_id=?', @project.id, @project.id, @project.id]) + if @project && @project.project? && is_admin?(@project) + Project.delete_project(@project) end end redirect_to_default end + def history + @project=Project.by_key(params[:id]) + return access_denied unless is_admin?(@project) + + if !(@project.project? || @project.view? || @project.subview?) + redirect_to :action => 'index', :id => params[:id] + end + + @snapshot=@project.last_snapshot + @snapshots = Snapshot.find(:all, :conditions => ["status='P' AND project_id=?", @project.id], + :include => 'events', :order => 'snapshots.created_at DESC') + end + + def delete_snapshot_history + project=Project.by_key(params[:id]) + return access_denied unless is_admin?(@project) + + sid = params[:snapshot_id] + if sid + Snapshot.update_all("status='U'", ["id=? or root_snapshot_id=(?)", sid.to_i, sid.to_i]) + flash[:notice] = message('project_history.snapshot_deleted') + end + + redirect_to :action => 'history', :id => project.id + end + + def links + @project=Project.by_key(params[:id]) + return access_denied unless is_admin?(@project) + + @snapshot=@project.last_snapshot + if !@project.project? + redirect_to :action => 'index', :id => params[:id] + end + end + def set_links project = Project.by_key(params[:project_id]) return access_denied unless is_admin?(project) @@ -61,9 +106,10 @@ class ProjectController < ApplicationController project.save! flash[:notice] = 'Links updated.' - redirect_to :action => 'settings', :id => project.id + redirect_to :action => 'links', :id => project.id end + def settings @project=Project.by_key(params[:id]) return access_denied unless is_admin?(@project) @@ -72,6 +118,19 @@ class ProjectController < ApplicationController if !@project.project? && !@project.module? redirect_to :action => 'index', :id => params[:id] end + + @category=params[:category] ||= 'general' + @properties_per_category={} + java_facade.getPluginsMetadata().each do |plugin| + properties=java_facade.getPluginProperties(plugin).select { |property| + (@project.module? && property.module()) || (@project.project? && property.project()) + } + properties.each do |property| + category = (property.category().present? ? property.category() : plugin.name()) + @properties_per_category[category]||=[] + @properties_per_category[category]<<property + end + end end @@ -90,6 +149,16 @@ class ProjectController < ApplicationController end + def exclusions + @project=Project.by_key(params[:id]) + return access_denied unless is_admin?(@project) + + @snapshot=@project.last_snapshot + if !@project.project? && !@project.module? + redirect_to :action => 'index', :id => params[:id] + end + end + def set_exclusions @project = Project.find(params[:id]) return access_denied unless is_admin?(@project) @@ -102,7 +171,7 @@ class ProjectController < ApplicationController Property.set('sonar.exclusions', patterns.collect{|x| x.strip}.join(','), @project.id) end flash[:notice]='Filters added' - redirect_to :action => 'settings', :id => @project.id + redirect_to :action => 'exclusions', :id => @project.id end def delete_exclusions @@ -111,7 +180,7 @@ class ProjectController < ApplicationController Property.clear('sonar.exclusions', @project.id) flash[:notice]='Filters deleted' - redirect_to :action => 'settings', :id => @project.id + redirect_to :action => 'exclusions', :id => @project.id end protected diff --git a/sonar-server/src/main/webapp/WEB-INF/app/controllers/resource_controller.rb b/sonar-server/src/main/webapp/WEB-INF/app/controllers/resource_controller.rb index 8cdc48be703..2ca2eadfa51 100644 --- a/sonar-server/src/main/webapp/WEB-INF/app/controllers/resource_controller.rb +++ b/sonar-server/src/main/webapp/WEB-INF/app/controllers/resource_controller.rb @@ -55,10 +55,14 @@ class ResourceController < ApplicationController def load_extensions @extensions=[] java_facade.getResourceTabs(@resource.scope, @resource.qualifier, @resource.language).each do |tab| - tab.getUserRoles().each do |role| - if has_role?(role, @resource) - @extensions<<tab - break + if tab.getUserRoles().empty? + @extensions<<tab + else + tab.getUserRoles().each do |role| + if has_role?(role, @resource) + @extensions<<tab + break + end end end end @@ -69,7 +73,6 @@ class ResourceController < ApplicationController elsif !params[:metric].blank? metric=Metric.by_key(params[:metric]) @extension=@extensions.find{|extension| extension.getDefaultTabForMetrics().include?(metric.key)} - end @extension=@extensions.find{|extension| extension.isDefaultTab()} if @extension==nil end diff --git a/sonar-server/src/main/webapp/WEB-INF/app/controllers/server_id_configuration_controller.rb b/sonar-server/src/main/webapp/WEB-INF/app/controllers/server_id_configuration_controller.rb new file mode 100644 index 00000000000..9f77056ad06 --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/app/controllers/server_id_configuration_controller.rb @@ -0,0 +1,60 @@ +# +# Sonar, entreprise quality control tool. +# Copyright (C) 2008-2011 SonarSource +# mailto:contact AT sonarsource DOT com +# +# Sonar is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 3 of the License, or (at your option) any later version. +# +# Sonar is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with Sonar; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 +# +class ServerIdConfigurationController < ApplicationController + + SECTION=Navigation::SECTION_CONFIGURATION + PROPERTY_SERVER_ID = 'sonar.server_id' + PROPERTY_IP_ADDRESS = 'sonar.server_id.ip_address' + PROPERTY_ORGANISATION = 'sonar.organisation' + + before_filter :admin_required + verify :method => :post, :only => [:generate], :redirect_to => {:action => :index} + + def index + @server_id = Property.value(PROPERTY_SERVER_ID) + @organisation = Property.value(PROPERTY_ORGANISATION) + @address = Property.value(PROPERTY_IP_ADDRESS) + @valid_addresses = java_facade.getValidInetAddressesForServerId() + @bad_id = false + if @server_id.present? + id = java_facade.generateServerId(@organisation, @address) + @bad_id = (@server_id != id) + end + params[:layout]='false' + end + + def generate + organisation = params[:organisation] + Property.set(PROPERTY_ORGANISATION, organisation) + + ip_address=params[:address] + Property.set(PROPERTY_IP_ADDRESS, ip_address) + + id = java_facade.generateServerId(organisation, ip_address) + if id + Property.set(PROPERTY_SERVER_ID, id) + else + Property.clear(PROPERTY_SERVER_ID) + flash[:error] = Api::Utils.message('server_id_configuration.generation_error') + end + + redirect_to :action => 'index' + end +end diff --git a/sonar-server/src/main/webapp/WEB-INF/app/controllers/settings_controller.rb b/sonar-server/src/main/webapp/WEB-INF/app/controllers/settings_controller.rb index 854c5f746db..d3d27285c9e 100644 --- a/sonar-server/src/main/webapp/WEB-INF/app/controllers/settings_controller.rb +++ b/sonar-server/src/main/webapp/WEB-INF/app/controllers/settings_controller.rb @@ -20,26 +20,31 @@ class SettingsController < ApplicationController SECTION=Navigation::SECTION_CONFIGURATION + + SPECIAL_CATEGORIES=['email', 'server_id'] - verify :method => :post, :only => ['update'], :redirect_to => { :action => :index } + verify :method => :post, :only => ['update'], :redirect_to => {:action => :index} def index - return access_denied unless is_admin? + return access_denied unless is_admin? + load_properties(false) + @category ||= 'general' end def update if params[:resource_id] project=Project.by_key(params[:resource_id]) - return access_denied unless is_admin?(project) + return access_denied unless (project && is_admin?(project)) resource_id=project.id else return access_denied unless is_admin? + resource_id=nil end - plugins = java_facade.getPluginsMetadata() - plugins.each do |plugin| - properties=java_facade.getPluginProperties(plugin) - properties.each do |property| + load_properties(true) + + if @category && @properties_per_category[@category] + @properties_per_category[@category].each do |property| value=params[property.key()] persisted_property = Property.find(:first, :conditions => {:prop_key=> property.key(), :resource_id => resource_id, :user_id => nil}) @@ -48,20 +53,37 @@ class SettingsController < ApplicationController Property.delete_all('prop_key' => property.key(), 'resource_id' => resource_id, 'user_id' => nil) elsif persisted_property.text_value != value.to_s persisted_property.text_value = value.to_s - persisted_property.save + persisted_property.save! end - elsif !value.blank? + elsif !value.blank? Property.create(:prop_key => property.key(), :text_value => value.to_s, :resource_id => resource_id) end end + java_facade.reloadConfiguration() + flash[:notice] = 'Parameters updated' end - java_facade.reloadConfiguration() - flash[:notice] = 'Parameters updated.' if resource_id - redirect_to :controller => 'project', :action => 'settings', :id => resource_id + redirect_to :controller => 'project', :action => 'settings', :id => resource_id, :category => @category else - redirect_to :action => 'index' + redirect_to :controller => 'settings', :action => 'index', :category => @category + end + end + + private + + def load_properties(all=true) + @category=params[:category] + @properties_per_category={} + java_facade.getPluginsMetadata().each do |plugin| + java_facade.getPluginProperties(plugin).select { |property| all || property.global }.each do |property| + category = (property.category().present? ? property.category() : plugin.name()) + @properties_per_category[category]||=[] + @properties_per_category[category]<<property + end + end + SPECIAL_CATEGORIES.each do |category| + @properties_per_category[category]=[] end end end diff --git a/sonar-server/src/main/webapp/WEB-INF/app/controllers/timemachine_controller.rb b/sonar-server/src/main/webapp/WEB-INF/app/controllers/timemachine_controller.rb index 47d0d223998..30fae8bef84 100644 --- a/sonar-server/src/main/webapp/WEB-INF/app/controllers/timemachine_controller.rb +++ b/sonar-server/src/main/webapp/WEB-INF/app/controllers/timemachine_controller.rb @@ -29,6 +29,7 @@ class TimemachineController < ApplicationController def index @project = Project.by_key(params[:id]) + return redirect_to home_url unless @project @snapshot=@project.last_snapshot return access_denied unless is_user?(@snapshot) diff --git a/sonar-server/src/main/webapp/WEB-INF/app/controllers/users_controller.rb b/sonar-server/src/main/webapp/WEB-INF/app/controllers/users_controller.rb index fa7672b617f..d39d7fda380 100644 --- a/sonar-server/src/main/webapp/WEB-INF/app/controllers/users_controller.rb +++ b/sonar-server/src/main/webapp/WEB-INF/app/controllers/users_controller.rb @@ -32,7 +32,7 @@ class UsersController < ApplicationController if user.save flash[:notice] = 'User is created.' end - + to_index(user.errors, nil); end @@ -68,12 +68,31 @@ class UsersController < ApplicationController redirect_to(:action => 'index', :id => params[:id]) end + def change_password + @users = User.find(:all, :include => 'groups', :order => 'name') + @user = User.find(params[:id]) + render :action => 'index', :id => params[:id] + end + + def update_password + user = User.find(params[:id]) + + if params[:user][:password].blank? + flash[:error] = 'Password required.' + + elsif user.update_attributes(:password => params[:user][:password], :password_confirmation => params[:user][:password_confirmation]) + flash[:notice] = 'Password was successfully updated.' + end + + to_index(user.errors, nil); + end + def update user = User.find(params[:id]) if user.login!=params[:user][:login] flash[:error] = 'Login can not be changed.' - + elsif user.update_attributes(params[:user]) flash[:notice] = 'User was successfully updated.' end @@ -100,7 +119,7 @@ class UsersController < ApplicationController def set_groups @user = User.find(params[:id]) - + if @user.set_groups(params[:groups]) flash[:notice] = 'User is updated.' end @@ -115,7 +134,7 @@ class UsersController < ApplicationController redirect_to(:action => 'index', :id => id) end - + def toggle_edit_mode() current_user.toggle_edit_mode redirect_back_or_default(:controller => 'project') @@ -129,11 +148,11 @@ class UsersController < ApplicationController user.groups<<default_group if default_group user end - + def autocomplete @users = User.find(:all, :conditions => ["UPPER(name) like ?", params[:user_name_start].clone.upcase+"%"]) @char_count = params[:user_name_start].size render :partial => 'autocomplete' end - + end diff --git a/sonar-server/src/main/webapp/WEB-INF/app/models/project.rb b/sonar-server/src/main/webapp/WEB-INF/app/models/project.rb index 5c7273c0296..01b358e17ce 100644 --- a/sonar-server/src/main/webapp/WEB-INF/app/models/project.rb +++ b/sonar-server/src/main/webapp/WEB-INF/app/models/project.rb @@ -20,7 +20,7 @@ class Project < ActiveRecord::Base include Comparable include Resourceable - + has_many :snapshots has_many :processed_snapshots, :class_name => 'Snapshot', :conditions => "status='#{Snapshot::STATUS_PROCESSED}' AND qualifier<>'LIB'", :order => 'created_at asc' has_many :events, :foreign_key => 'resource_id', :order => 'event_date DESC' @@ -39,33 +39,40 @@ class Project < ActiveRecord::Base Project.find(:first, :conditions => {:kee => k}) end end - + + def self.delete_project(project) + if project + Snapshot.update_all(['islast=?', false], ['(root_project_id=? OR project_id=?) AND islast=?', project.id, project.id, true]) + Project.delete_all(['id=? OR root_id=? or copy_resource_id=?', project.id, project.id, project.id]) + end + end + def project root||self end - + def root_project - @root_project ||= - begin - parent_module(self) - end + @root_project ||= + begin + parent_module(self) + end end - + def last_snapshot @last_snapshot ||= - begin - snapshot=Snapshot.find(:first, :conditions => {:islast => true, :project_id => id}) - if snapshot - snapshot.project=self + begin + snapshot=Snapshot.find(:first, :conditions => {:islast => true, :project_id => id}) + if snapshot + snapshot.project=self + end + snapshot end - snapshot - end end - + def events_with_snapshot - events.select{|event| !event.snapshot_id.nil?} + events.select { |event| !event.snapshot_id.nil? } end - + def key kee end @@ -73,29 +80,29 @@ class Project < ActiveRecord::Base def links project_links end - + def link(type) # to_a avoids conflicts with ActiveRecord:Base.find - links.to_a.find{ |l| l.link_type==type} - end - + links.to_a.find { |l| l.link_type==type } + end + def custom_links - links.select {|l| l.custom?} + links.select { |l| l.custom? } end - + def standard_links - links.reject {|l| l.custom?} + links.reject { |l| l.custom? } end def chart_measures(metric_id) sql = Project.send(:sanitize_sql, ['select s.created_at as created_at, m.value as value ' + - ' from project_measures m, snapshots s ' + - ' where s.id=m.snapshot_id and ' + - " s.status='%s' and " + - ' s.project_id=%s and m.metric_id=%s ', Snapshot::STATUS_PROCESSED, self.id, metric_id] ) + - ' and m.rule_id IS NULL and m.rule_priority IS NULL' + - ' order by s.created_at' - create_chart_measures( Project.connection.select_all( sql ), 'created_at', 'value' ) + ' from project_measures m, snapshots s ' + + ' where s.id=m.snapshot_id and ' + + " s.status='%s' and " + + ' s.project_id=%s and m.metric_id=%s ', Snapshot::STATUS_PROCESSED, self.id, metric_id]) + + ' and m.rule_id IS NULL and m.rule_priority IS NULL' + + ' order by s.created_at' + create_chart_measures(Project.connection.select_all(sql), 'created_at', 'value') end def <=>(other) @@ -113,13 +120,13 @@ class Project < ActiveRecord::Base def fullname name end - + def branch if project? || module? s=kee.split(':') if s.size>=3 return s[2] - end + end end nil end @@ -151,19 +158,19 @@ class Project < ActiveRecord::Base # when regular active record impl return string typed objects if results.first[date_column_name].class == Time results.each do |hash| - chart_measures << ChartMeasure.new( hash[date_column_name], hash[value_column_name] ) + chart_measures << ChartMeasure.new(hash[date_column_name], hash[value_column_name]) end else results.each do |hash| - chart_measures << ChartMeasure.new( Time.parse( hash[date_column_name] ), hash[value_column_name].to_d ) + chart_measures << ChartMeasure.new(Time.parse(hash[date_column_name]), hash[value_column_name].to_d) end end end chart_measures end - + def parent_module(current_module) current_module.root ? parent_module(current_module.root) : current_module end - + end diff --git a/sonar-server/src/main/webapp/WEB-INF/app/models/property.rb b/sonar-server/src/main/webapp/WEB-INF/app/models/property.rb index f32b9bfc294..196d022d6fb 100644 --- a/sonar-server/src/main/webapp/WEB-INF/app/models/property.rb +++ b/sonar-server/src/main/webapp/WEB-INF/app/models/property.rb @@ -86,6 +86,6 @@ class Property < ActiveRecord::Base private def self.reload_java_configuration - Java::OrgSonarServerUi::JRubyFacade.new.reloadConfiguration() + Java::OrgSonarServerUi::JRubyFacade.getInstance().reloadConfiguration() end end diff --git a/sonar-server/src/main/webapp/WEB-INF/app/models/server.rb b/sonar-server/src/main/webapp/WEB-INF/app/models/server.rb index f0baab37f59..0e2aaad99bb 100644 --- a/sonar-server/src/main/webapp/WEB-INF/app/models/server.rb +++ b/sonar-server/src/main/webapp/WEB-INF/app/models/server.rb @@ -25,7 +25,7 @@ class Server def system_info system_info=[] - add_property(system_info, 'System date') {format_date(java.util.Date.new())} + add_property(system_info, 'System date') {java.util.Date.new()} add_property(system_info, 'JVM Vendor') {java.lang.management.ManagementFactory.getRuntimeMXBean().getVmVendor()} add_property(system_info, 'JVM Name') {java.lang.management.ManagementFactory.getRuntimeMXBean().getVmName()} add_property(system_info, 'JVM Version') {java.lang.management.ManagementFactory.getRuntimeMXBean().getVmVersion() } @@ -59,8 +59,9 @@ class Server def sonar_info sonar_info=[] + add_property(sonar_info, 'Server ID') {sonar_property(ServerIdConfigurationController::PROPERTY_SERVER_ID)} add_property(sonar_info, 'Version') {org.sonar.server.platform.Platform.getServer().getVersion()} - add_property(sonar_info, 'ID') {org.sonar.server.platform.Platform.getServer().getId()} + add_property(sonar_info, 'Started at') {org.sonar.server.platform.Platform.getServer().getStartedAt()} add_property(sonar_info, 'Database') {"#{jdbc_metadata. getDatabaseProductName()} #{jdbc_metadata. getDatabaseProductVersion()}"} add_property(sonar_info, 'Database URL') {sonar_property('sonar.jdbc.url')} add_property(sonar_info, 'Database Login') {sonar_property('sonar.jdbc.username')} diff --git a/sonar-server/src/main/webapp/WEB-INF/app/models/snapshot.rb b/sonar-server/src/main/webapp/WEB-INF/app/models/snapshot.rb index 5a6fd0ea838..7b5f9034fab 100644 --- a/sonar-server/src/main/webapp/WEB-INF/app/models/snapshot.rb +++ b/sonar-server/src/main/webapp/WEB-INF/app/models/snapshot.rb @@ -67,6 +67,36 @@ class Snapshot < ActiveRecord::Base snapshots.compact.uniq end + + def self.for_timemachine_widget(resource, number_of_columns, options={}) + if number_of_columns == 1 + # Display only the latest snapshot + return [resource.last_snapshot] + end + + # Get 1rst & latests snapshots of the period + snapshot_conditions = ["snapshots.project_id=? AND snapshots.status=? AND snapshots.scope=? AND snapshots.qualifier=?", resource.id, STATUS_PROCESSED, resource.scope, resource.qualifier] + if options[:from] + snapshot_conditions[0] += " AND snapshots.created_at>=?" + snapshot_conditions << options[:from] + end + first_snapshot=Snapshot.find(:first, :conditions => snapshot_conditions, :order => 'snapshots.created_at ASC') + last_snapshot=resource.last_snapshot + + if first_snapshot==last_snapshot + return [last_snapshot] + end + + # Look for the number_of_columns-2 last snapshots to display (they must have 'Version' events) + version_snapshots = [] + if number_of_columns > 2 + snapshot_conditions[0] += " AND events.snapshot_id=snapshots.id AND events.category='Version' AND snapshots.id NOT IN (?)" + snapshot_conditions << [first_snapshot.id, last_snapshot.id] + version_snapshots=Snapshot.find(:all, :conditions => snapshot_conditions, :include => 'events', :order => 'snapshots.created_at ASC').last(number_of_columns-2) + end + + return [first_snapshot] + version_snapshots + [last_snapshot] + end def last? islast diff --git a/sonar-server/src/main/webapp/WEB-INF/app/models/sonar/columns_view.rb b/sonar-server/src/main/webapp/WEB-INF/app/models/sonar/columns_view.rb index e2a8d1ce08d..47bd1c8f943 100644 --- a/sonar-server/src/main/webapp/WEB-INF/app/models/sonar/columns_view.rb +++ b/sonar-server/src/main/webapp/WEB-INF/app/models/sonar/columns_view.rb @@ -30,6 +30,19 @@ class Sonar::ColumnsView attr_accessor :id, :name, :col_type, :position, :sort_default + def name(translate=true) + return @name unless translate + + i18n_key = @id + return @name if i18n_key.nil? + + if metric_column? + i18n_key = 'metric.' + i18n_key + '.name' + end + + Api::Utils.message(i18n_key, :default => @name) + end + def project_column? @col_type == TYPE_PROJECT end diff --git a/sonar-server/src/main/webapp/WEB-INF/app/models/trends_chart.rb b/sonar-server/src/main/webapp/WEB-INF/app/models/trends_chart.rb index 44855d4077f..8c60ee3eed5 100644 --- a/sonar-server/src/main/webapp/WEB-INF/app/models/trends_chart.rb +++ b/sonar-server/src/main/webapp/WEB-INF/app/models/trends_chart.rb @@ -25,7 +25,6 @@ class TrendsChart init_series(java_chart, metrics) metric_ids=metrics.map{|m| m.id} add_measures(java_chart, time_machine_measures(resource, metric_ids, options)) - add_measures(java_chart, time_machine_reviews(resource, metric_ids, options)) add_labels(java_chart, resource); export_chart_as_png(java_chart) @@ -42,7 +41,7 @@ class TrendsChart def self.time_machine_measures(resource, metric_ids, options={}) unless metric_ids.empty? - sql= "select s.created_at as created_at, m.value as value, m.metric_id as metric_id " + + sql= "select s.created_at as created_at, m.value as value, m.metric_id as metric_id, s.id as sid " + " from project_measures m LEFT OUTER JOIN snapshots s ON s.id=m.snapshot_id " + " where m.rule_id is null " + " and s.status=? " + @@ -55,6 +54,7 @@ class TrendsChart if (options[:to]) sql += ' and s.created_at<=?' end + sql += ' order by s.created_at ASC' conditions=[sql, Snapshot::STATUS_PROCESSED, resource.id, metric_ids] if (options[:from]) conditions<<options[:from] @@ -65,33 +65,7 @@ class TrendsChart ProjectMeasure.connection.select_all(Project.send(:sanitize_sql, conditions)) end end - - def self.time_machine_reviews(resource, metric_ids, options={}) - unless metric_ids.empty? - sql= "SELECT m.measure_date as created_at, m.value as value, m.metric_id as metric_id " + - "FROM project_measures m " + - "WHERE m.snapshot_id IS NULL " + - "AND m.rule_id is null " + - "AND m.project_id=? " + - "AND m.metric_id in (?) " + - "and m.rule_priority is null and m.characteristic_id IS NULL" - if (options[:from]) - sql += ' and m.measure_date>=?' - end - if (options[:to]) - sql += ' and m.measure_date<=?' - end - conditions=[sql, resource.id, metric_ids] - if (options[:from]) - conditions<<options[:from] - end - if (options[:to]) - conditions<<options[:to] - end - ProjectMeasure.connection.select_all(Project.send(:sanitize_sql, conditions)) - end - end - + def self.add_measures(java_chart, sqlresult) if sqlresult && sqlresult.size>0 sqlresult.each do |hash| diff --git a/sonar-server/src/main/webapp/WEB-INF/app/models/user.rb b/sonar-server/src/main/webapp/WEB-INF/app/models/user.rb index 7635670b6b0..ac04601fe30 100644 --- a/sonar-server/src/main/webapp/WEB-INF/app/models/user.rb +++ b/sonar-server/src/main/webapp/WEB-INF/app/models/user.rb @@ -41,8 +41,9 @@ class User < ActiveRecord::Base validates_length_of :name, :maximum => 200, :allow_blank => true, :allow_nil => true validates_length_of :email, :maximum => 100, :allow_blank => true, :allow_nil => true - validates_length_of :password, :within => 4..40, :if => :password_required? - validates_confirmation_of :password, :if => :password_required? + # The following two validations not needed, because they come with Authentication::ByPassword - see SONAR-2656 + #validates_length_of :password, :within => 4..40, :if => :password_required? + #validates_confirmation_of :password, :if => :password_required? validates_presence_of :login validates_length_of :login, :within => 2..40 diff --git a/sonar-server/src/main/webapp/WEB-INF/app/views/autocomplete/_text_field.html.erb b/sonar-server/src/main/webapp/WEB-INF/app/views/autocomplete/_text_field.html.erb index 2e9b6f047e7..bd2913043b8 100644 --- a/sonar-server/src/main/webapp/WEB-INF/app/views/autocomplete/_text_field.html.erb +++ b/sonar-server/src/main/webapp/WEB-INF/app/views/autocomplete/_text_field.html.erb @@ -1,17 +1,22 @@ <input type="text" id="autocompleteText-<%= param_id_name -%>" - value="<%= param_displayed_value -%>" - onfocus="$('<%= param_id_name -%>').value=''; this.value=''" - <%= "class=\"" + options[:class] + "\"" if options[:class] -%>/> + value="<%= param_displayed_value -%>" + <%= "class=\"" + options[:class] + "\"" if options[:class] -%> + onKeyUp="if (this.value.length== 0 ||Â this.value.length== 1) $('<%= param_id_name -%>').value = '';"/> <input type="hidden" id="<%= param_id_name -%>" name="<%= param_id_name -%>" value="<%= param_id_value -%>"/> <div id="autocomplete-<%= param_id_name -%>" class="autocomplete"></div> <script> new Ajax.Autocompleter("autocompleteText-<%= param_id_name -%>", "autocomplete-<%= param_id_name -%>", "<%= server_url -%>", { paramName: "user_name_start", minChars: 2, - afterUpdateElement : getSelection<%= param_id_name -%> + afterUpdateElement : getSelection<%= param_id_name -%>, + callback : callBack<%= param_id_name -%> }); function getSelection<%= param_id_name -%>(text, li) { - $('<%= param_id_name -%>').value = li.id + $('<%= param_id_name -%>').value = li.id; + } + function callBack<%= param_id_name -%>(field, queryString) { + $('<%= param_id_name -%>').value = ''; + return queryString; } </script>
\ No newline at end of file diff --git a/sonar-server/src/main/webapp/WEB-INF/app/views/email_configuration/index.html.erb b/sonar-server/src/main/webapp/WEB-INF/app/views/email_configuration/index.html.erb index 04e91ee296c..f9abb2886ec 100644 --- a/sonar-server/src/main/webapp/WEB-INF/app/views/email_configuration/index.html.erb +++ b/sonar-server/src/main/webapp/WEB-INF/app/views/email_configuration/index.html.erb @@ -1,70 +1,87 @@ -<h1><%= message('email_configuration.page') -%></h1> -<div class="admin"> <% form_tag({:action => 'save'}) do -%> - <table class="form"> + <table class="data form marginbottom10"> + <thead> <tr> + <th><span><%= message('email_configuration.page') -%></span></th> + </tr> + </thead> + <tbody> + <tr class="even"> <td class="keyCell"><label for="smtp_host"><%= message('email_configuration.smtp_host') -%>:</label></td> <td><%= text_field_tag 'smtp_host', @smtp_host %></td> <td class="comments"><%= message('email_configuration.smtp_host.description') -%></td> </tr> - <tr> + <tr class="odd"> <td class="keyCell"><label for="smtp_port"><%= message('email_configuration.smtp_port') -%>:</label></td> <td><%= text_field_tag 'smtp_port', @smtp_port %></td> <td class="comments"><%= message('email_configuration.smtp_port.description') -%></td> </tr> - <tr> + <tr class="even"> <td class="keyCell"><label for="smtp_secure_connection"><%= message('email_configuration.smtp_secure_connection') -%>:</label></td> <td><%= select_tag 'smtp_secure_connection', options_for_select({'No' => '', 'SSL' => 'ssl'}, @smtp_secure_connection) %></td> <td class="comments"><%= message('email_configuration.smtp_secure_connection.description') -%></td> </tr> - <tr> + <tr class="odd"> <td class="keyCell"><label for="smtp_username"><%= message('email_configuration.smtp_username') -%>:</label></td> <td><%= text_field_tag 'smtp_username', @smtp_username %></td> <td class="comments"><%= message('email_configuration.smtp_username.description') -%></td> </tr> - <tr> + <tr class="even"> <td class="keyCell"><label for="smtp_password"><%= message('email_configuration.smtp_password') -%>:</label></td> <td><%= password_field_tag 'smtp_password', @smtp_password %></td> <td class="comments"><%= message('email_configuration.smtp_password.description') -%></td> </tr> - <tr> + <tr class="odd"> <td class="keyCell"><label for="email_from"><%= message('email_configuration.from_address') -%>:</label></td> <td><%= text_field_tag 'email_from', @email_from %></td> <td class="comments"><%= message('email_configuration.from_address.description') -%></td> </tr> - <tr> + <tr class="even"> <td class="keyCell"><label for="email_prefix"><%= message('email_configuration.email_prefix') -%>:</label></td> <td><%= text_field_tag 'email_prefix', @email_prefix %></td> <td class="comments"><%= message('email_configuration.email_prefix.description') -%></td> </tr> + </tbody> + <tfoot> <tr> - <td></td> - <td><%= submit_tag message('email_configuration.save_settings'), :disable_with => message('email_configuration.saving_settings') %></td> + <td colspan="3"> + <%= submit_tag message('email_configuration.save_settings'), :disable_with => message('email_configuration.saving_settings'), :id => 'submit_save' %> + </td> </tr> + </tfoot> </table> <% end -%> -</div> -<h1><%= message('email_configuration.test.title') -%></h1> -<div class="admin"> +<br/> + <% form_tag({:action => 'send_test_email'}) do -%> - <table class="form"> + <table class="data form marginbottom10"> + <thead> <tr> + <th><span><%= message('email_configuration.test.title') -%></span></th> + </tr> + </thead> + <tfoot> + <tr> + <td colspan="3"> + <%= submit_tag message('email_configuration.test.send'), :disable_with => message('email_configuration.test.sending'), :id => 'submit_test' %> + </td> + </tr> + </tfoot> + <tbody> + <tr class="even"> <td class="keyCell"><label for="to_address"><%= message('email_configuration.test.to_address') -%>:</label></td> <td><%= text_field_tag 'to_address', current_user.email %></td> </tr> - <tr> + <tr class="odd"> <td class="keyCell"><label for="subject"><%= message('email_configuration.test.subject') -%>:</label></td> <td><%= text_field_tag 'subject', message('email_configuration.test.subject_text') %></td> </tr> - <tr> + <tr class="even"> <td class="keyCell"><label for="message"><%= message('email_configuration.test.message') -%>:</label></td> <td><%= text_area_tag 'message', message('email_configuration.test.message_text'), {:cols => 40, :rows => 6} %></td> </tr> - <tr> - <td></td> - <td><%= submit_tag message('email_configuration.test.send'), :disable_with => message('email_configuration.test.sending') %></td> - </tr> + </tbody> </table> <% end -%> -</div> + diff --git a/sonar-server/src/main/webapp/WEB-INF/app/views/filters/_list.html.erb b/sonar-server/src/main/webapp/WEB-INF/app/views/filters/_list.html.erb index 78b86a04143..762d8e3a30a 100644 --- a/sonar-server/src/main/webapp/WEB-INF/app/views/filters/_list.html.erb +++ b/sonar-server/src/main/webapp/WEB-INF/app/views/filters/_list.html.erb @@ -86,7 +86,7 @@ </tr> <% end %> <% if @filter_context.empty? %> - <tr class="even"><td colspan="<%= 1+filter.columns.size -%>">No results.</td></tr> + <tr class="even"><td colspan="<%= 1+filter.columns.size -%>"><%= message('no_results') -%></td></tr> <% else %> <% @filter_context.page_sorted_snapshot_ids.each do |snapshot_id| diff --git a/sonar-server/src/main/webapp/WEB-INF/app/views/layouts/_head.html.erb b/sonar-server/src/main/webapp/WEB-INF/app/views/layouts/_head.html.erb index 36d9d613da0..50ee96547cb 100644 --- a/sonar-server/src/main/webapp/WEB-INF/app/views/layouts/_head.html.erb +++ b/sonar-server/src/main/webapp/WEB-INF/app/views/layouts/_head.html.erb @@ -14,25 +14,28 @@ %> <title><%= title %></title> <% if ENV['RAILS_ENV'] == 'production' -%><%= stylesheet_link_tag 'sonar', :media => 'all' %><%= javascript_include_tag 'sonar' -%><% else %> +%><%= stylesheet_link_tag 'sonar', :media => 'all' -%><%= javascript_include_tag 'sonar' +-%><% else %> <%= stylesheet_link_tag 'yui-2.6.0.css', :media => 'all' %> <%= stylesheet_link_tag 'calendar', :media => 'all' %> <%= stylesheet_link_tag 'style', :media => 'all' %> <%= stylesheet_link_tag 'sonar-colorizer', :media => 'all' %> <%= stylesheet_link_tag 'dashboard', :media => 'all' %> -<%= javascript_include_tag 'calendar/yahoo-dom-event.js' %> -<%= javascript_include_tag 'calendar/calendar.js' %> <%= javascript_include_tag 'application' %> <%= javascript_include_tag 'prototype' %> <%= javascript_include_tag 'scriptaculous' %> <%= javascript_include_tag 'tablekit' %> <%= javascript_include_tag 'prototip' %> <%= javascript_include_tag 'dashboard' %> +<%= javascript_include_tag 'protovis' %> +<%= javascript_include_tag 'protovis-sonar' %> <% end %> <!--[if lte IE 6]> <link href="<%= ApplicationController.root_context -%>/ie6/index" media="all" rel="stylesheet" type="text/css" /> <![endif]--> +<!--[if lt IE 9]> +<%= javascript_include_tag 'protovis-msie-shim' -%> +<![endif]--> <link rel="shortcut icon" type="image/x-icon" href="<%= image_path('favicon.ico') -%>" /> <% if @project %> diff --git a/sonar-server/src/main/webapp/WEB-INF/app/views/layouts/_layout.html.erb b/sonar-server/src/main/webapp/WEB-INF/app/views/layouts/_layout.html.erb index 871641fa03a..d196fb6cb0c 100644 --- a/sonar-server/src/main/webapp/WEB-INF/app/views/layouts/_layout.html.erb +++ b/sonar-server/src/main/webapp/WEB-INF/app/views/layouts/_layout.html.erb @@ -54,20 +54,32 @@ <% if (@project.project? || @project.module?) %> <li class="<%= 'selected' if request.request_uri.include?('/project/settings') -%>"><a href="<%= ApplicationController.root_context -%>/project/settings/<%= @project.id -%>"><%= message('project_settings.page') -%></a></li> <% end %> + <% if (@project.project? || @project.module?) %> + <li class="<%= 'selected' if request.request_uri.include?('/project/exclusions') -%>"><a href="<%= ApplicationController.root_context -%>/project/exclusions/<%= @project.id -%>"><%= message('project_exclusions.page') -%></a></li> + <% end %> + <% if (@project.project?) %> + <li class="<%= 'selected' if request.request_uri.include?('/project/links') -%>"><a href="<%= ApplicationController.root_context -%>/project/links/<%= @project.id -%>"><%= message('project_links.page') -%></a></li> + <% end %> <% if (@project.project? || @project.view? || @project.subview?) %> <li class="<%= 'selected' if request.request_uri.include?('/project_roles') -%>"><a href="<%= ApplicationController.root_context -%>/project_roles/index?resource=<%= @project.id -%>"><%= message('project_roles.page') -%></a></li> + <li class="<%= 'selected' if request.request_uri.include?('/project/history') -%>"><a href="<%= ApplicationController.root_context -%>/project/history/<%= @project.id -%>"><%= message('project_history.page') -%></a></li> + <% end %> + <% if (@project.project?) %> + <li class="<%= 'selected' if request.request_uri.include?('/project/deletion') -%>"><a href="<%= ApplicationController.root_context -%>/project/deletion/<%= @project.id -%>"><%= message('project_deletion.page') -%></a></li> <% end %> <% end %> <% elsif selected_section==Navigation::SECTION_CONFIGURATION %> <li class="<%= 'selected' if request.request_uri.include?('/profiles') || request.request_uri.include?('/rules_configuration') -%>"><a href="<%= ApplicationController.root_context -%>/profiles"><%= message('quality_profiles.page') -%></a></li> + <% if current_user %> + <li class="<%= 'selected' if controller.controller_path=='account' -%>"><a href="<%= ApplicationController.root_context -%>/account/index"><%= message('my_profile.page') -%></a></li> + <% end %> <% if is_admin? %> <li class="<%= 'selected' if controller.controller_path=='event_categories' -%>"><a href="<%= ApplicationController.root_context -%>/event_categories/index"><%= message('event_categories.page') -%></a></li> <li class="<%= 'selected' if controller.controller_path=='metrics' -%>"><a href="<%= ApplicationController.root_context -%>/metrics/index"><%= message('manual_metrics.page') -%></a></li> <li class="<%= 'selected' if controller.controller_path=='admin_filters' -%>"><a href="<%= ApplicationController.root_context -%>/admin_filters/index"><%= message('default_filters.page') -%></a></li> <li class="<%= 'selected' if controller.controller_path=='admin_dashboards' -%>"><a href="<%= ApplicationController.root_context -%>/admin_dashboards/index"><%= message('default_dashboards.page') -%></a></li> - <li class="<%= 'selected' if controller.controller_path=='account' -%>"><a href="<%= ApplicationController.root_context -%>/account/index"><%= message('my_profile.page') -%></a></li> <% controller.java_facade.getPages(Navigation::SECTION_CONFIGURATION, nil,nil, nil).each do |page| %> <li class="<%= 'selected' if request.request_uri.include?("plugins/configuration/#{page.getId()}") -%>"><a href="<%= ApplicationController.root_context -%>/plugins/configuration/<%= page.getId() -%>"><%= message(page.getId() + '.page', :default => page.getTitle()) %></a></li> <% end %> @@ -80,7 +92,6 @@ <li class="h2"><%= message('sidebar.system') -%></li> <li class="<%= 'selected' if request.request_uri.include?('/settings') -%>"><a href="<%= ApplicationController.root_context -%>/settings/index"><%= message('settings.page') -%></a></li> - <li class="<%= 'selected' if controller.controller_path=='email_configuration' -%>"><a href="<%= ApplicationController.root_context -%>/email_configuration"><%= message('email_configuration.page') -%></a></li> <li class="<%= 'selected' if controller.controller_path=='backup' -%>"><a href="<%= ApplicationController.root_context -%>/backup"><%= message('backup.page') -%></a></li> <li class="<%= 'selected' if controller.controller_path=='system' -%>"><a href="<%= ApplicationController.root_context -%>/system"><%= message('system_info.page') -%></a></li> <% update_center_activated = controller.java_facade.getConfigurationValue('sonar.updatecenter.activate') || 'true'; @@ -113,17 +124,7 @@ <% if footer.getHtml() %><div><%= footer.getHtml().to_s %></div><% end %> <% end %> <div id="ftlinks"> - <%= message('layout.powered_by') -%> <a href="http://www.sonarsource.com" target="SonarSource" class="external">SonarSource</a> - - - Open Source <a href="http://www.sonarsource.org/documentation/license/" target="license" class="external">LGPL</a> - - - v.<%= sonar_version -%> - - - <a href="http://sonar-plugins.codehaus.org" class="external" target="plugins"><%= message('layout.plugins') -%></a> - - - <a href="http://sonar.codehaus.org/documentation" class="external" target="sonar_doc" class="external"><%= message('layout.documentation') -%></a> - - - <a href="http://sonar.codehaus.org/support/" target="support" class="external"><%= message('layout.ask_a_questions') -%></a> + <%= message('layout.powered_by') -%> <a href="http://www.sonarsource.com" target="SonarSource" class="external">SonarSource</a> - Open Source <a href="http://www.sonarsource.org/documentation/license/" target="license" class="external">LGPL</a> - v.<%= sonar_version -%> - <a href="http://sonar-plugins.codehaus.org" class="external" target="plugins"><%= message('layout.plugins') -%></a> - <a href="http://sonar.codehaus.org/documentation" class="external" target="sonar_doc" class="external"><%= message('layout.documentation') -%></a> - <a href="http://sonar.codehaus.org/support/" target="support" class="external"><%= message('layout.ask_a_questions') -%></a> </div> </div> <% end %> diff --git a/sonar-server/src/main/webapp/WEB-INF/app/views/profiles/_tabs.html.erb b/sonar-server/src/main/webapp/WEB-INF/app/views/profiles/_tabs.html.erb index 03015f5d72a..0ea41f79e16 100644 --- a/sonar-server/src/main/webapp/WEB-INF/app/views/profiles/_tabs.html.erb +++ b/sonar-server/src/main/webapp/WEB-INF/app/views/profiles/_tabs.html.erb @@ -1,4 +1,4 @@ -<%= +<% new_tab = nil unless defined?(:new_tab) selected_tab = nil unless defined?(:selected_tab) %> diff --git a/sonar-server/src/main/webapp/WEB-INF/app/views/profiles/projects.html.erb b/sonar-server/src/main/webapp/WEB-INF/app/views/profiles/projects.html.erb index ce16627b5db..dec38c67172 100644 --- a/sonar-server/src/main/webapp/WEB-INF/app/views/profiles/projects.html.erb +++ b/sonar-server/src/main/webapp/WEB-INF/app/views/profiles/projects.html.erb @@ -43,6 +43,7 @@ SelectBox.init('to'); <% if @profile.projects.empty? %> <p><%= message('quality_profiles.no_projects_associated_to_profile_x', :params => @profile.name) -%></p> <% else %> + <p><%= message('quality_profiles.projects_warning') -%></p> <ol> <% @profile.projects.each do |project| %> <li><%= project.name %></li> diff --git a/sonar-server/src/main/webapp/WEB-INF/app/views/project/settings/_delete_project.html.erb b/sonar-server/src/main/webapp/WEB-INF/app/views/project/deletion.html.erb index 7e89d570273..a145f9eb4db 100644 --- a/sonar-server/src/main/webapp/WEB-INF/app/views/project/settings/_delete_project.html.erb +++ b/sonar-server/src/main/webapp/WEB-INF/app/views/project/deletion.html.erb @@ -1,5 +1,6 @@ <% if @snapshot.root? %> -<h2>Delete project</h2> +<h1>Delete project</h1> +<br/> <div class="yui-g widget" id="widget_delete_project"> <div class="warning"> This operation can not be undone. diff --git a/sonar-server/src/main/webapp/WEB-INF/app/views/project/settings/_exclusions.html.erb b/sonar-server/src/main/webapp/WEB-INF/app/views/project/exclusions.html.erb index 2e315bc8248..44130a13176 100644 --- a/sonar-server/src/main/webapp/WEB-INF/app/views/project/settings/_exclusions.html.erb +++ b/sonar-server/src/main/webapp/WEB-INF/app/views/project/exclusions.html.erb @@ -1,4 +1,5 @@ -<h2>Exclude sources from code analysis</h2> +<h1>Exclude sources from code analysis</h1> +<br/> <div class="yui-g widget" id="widget_exclusions"> @@ -27,8 +28,9 @@ <span class="note"><ul><li>com/mycompany/**/*.java</li><li>**/*Dummy.java</li></ul></span> </td></tr> <tr><td class=left> - <%= submit_tag( "Save filters", :id => 'submit_exclusions') %> - <%= link_to 'Delete all filters', {:action => 'delete_exclusions', :id => @project.id}, :method => 'POST', :confirm => "Are you sure you want to delete all exclusion filters ?", :id => 'delete_exclusions' %> + <%= submit_tag( "Save exclusion filters", :id => 'submit_exclusions') %> + <input type="submit" value="Delete all exclusion filters" id="delete_exclusions" class="action red-button" + onclick="if (confirm('Are you sure you want to delete all exclusion filters ?')) { var f = document.createElement('form'); f.style.display = 'none'; this.parentNode.appendChild(f); f.method = 'POST'; f.action = '<%= url_for :action => "delete_exclusions", :id => @project.id -%>';f.submit(); };return false;"> </td></tr> </table> <% end %> diff --git a/sonar-server/src/main/webapp/WEB-INF/app/views/project/history.html.erb b/sonar-server/src/main/webapp/WEB-INF/app/views/project/history.html.erb new file mode 100644 index 00000000000..c0be0cb0b8d --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/app/views/project/history.html.erb @@ -0,0 +1,53 @@ +<% if @snapshot.root? %> +<h1><%= message('project_history.page_title') -%></h1> +<br/> + +<table class="data" style="width:1%"> + <thead> + <tr> + <th class="thin nowrap"><%= message('project_history.col.year') -%></th> + <th class="thin nowrap"><%= message('project_history.col.month') -%></th> + <th class="thin nowrap" style="padding-left: 20px;"><%= message('project_history.col.events') -%></th> + <th class="thin nowrap" style="padding-left: 20px;"><%= message('project_history.col.time') -%></th> + <th class="thin nowrap center"><%= message('project_history.col.action') -%></th> + </tr> + </thead> + <tbody> + <% + current_year = nil + current_month = nil + @snapshots.each do |snapshot| + number_of_events = snapshot.events.size + time = snapshot.created_at + %> + <tr class="<%= cycle 'even','odd' -%>"> + <td class="thin nowrap"><b><%= time.year unless time.year == current_year -%></b></td> + <td class="thin nowrap"><b><%= l(time, :format => '%B').capitalize unless time.month == current_month -%></b></td> + <td class="thin nowrap" style="padding-left: 20px;"> + <%= snapshot.events.map{|e| e.name}.join(', ') -%> + </td> + <td class="thin nowrap" style="padding-left: 20px;"><%= l time, :format => :long -%></td> + <td class="thin nowrap center" style="padding-left:10px; padding-right:10px"> + <% + cell_content = nil; + if snapshot.islast? + cell_content = "<b>" + message('project_history.last_snapshot') + "</b>" + else + cell_content = button_to( message('project_history.delete_snapshot'), + { :action => "delete_snapshot_history", :id => @project.id, :snapshot_id => snapshot.id }, + :class => 'action red-button', + :confirm => message('project_history.are_you_sure_delete_snapshot_x', :params => l(time, :format => :long)) ) + end + %> + <%= cell_content -%> + </td> + </tr> + <% + current_year = time.year + current_month = time.month + end + %> + </tbody> +</table> + +<% end %>
\ No newline at end of file diff --git a/sonar-server/src/main/webapp/WEB-INF/app/views/project/settings/_links.html.erb b/sonar-server/src/main/webapp/WEB-INF/app/views/project/links.html.erb index 4ae38c09f8d..513bbe91ca6 100644 --- a/sonar-server/src/main/webapp/WEB-INF/app/views/project/settings/_links.html.erb +++ b/sonar-server/src/main/webapp/WEB-INF/app/views/project/links.html.erb @@ -1,4 +1,5 @@ -<h2>Project links</h2> +<h1>Project links</h1> +<br/> <div class="yui-g widget" id="widget_links"> <% form_for( 'set_links', :url => { :action => 'set_links', :project_id => @project.id } ) do |form| @@ -56,8 +57,9 @@ <%= text_field_tag( "url_scm_dev", links_by_key['scm_dev'] ? links_by_key['scm_dev'].href : '', :readonly => true, :size => 30 ) -%> </td> </tr> -<tr><td colspan="2" class="even"><%= submit_tag( "Save links" ) %></td></tr> </table> + <br/> + <%= submit_tag( "Save links" ) %> </div> <div class="yui-u"> <table class="data"> diff --git a/sonar-server/src/main/webapp/WEB-INF/app/views/project/settings.html.erb b/sonar-server/src/main/webapp/WEB-INF/app/views/project/settings.html.erb index 16f30eb7022..cb5e508f23b 100644 --- a/sonar-server/src/main/webapp/WEB-INF/app/views/project/settings.html.erb +++ b/sonar-server/src/main/webapp/WEB-INF/app/views/project/settings.html.erb @@ -1,13 +1,9 @@ <div name="settings"> -<%= render :partial => 'project/settings/plugins' %> -<br/> -<%= render :partial => 'project/settings/exclusions' %> -<% if @project.project? %> -<br/> -<%= render :partial => 'project/settings/links' %> -<br/> -<%= render :partial => 'project/settings/delete_project' %> -<% end %> + <h1 class="marginbottom10">Settings</h1> + + <div class="yui-g widget" id="widget_plugins"> + <%= render :partial => 'settings/plugins', :locals => {:project=>@project} %> + </div> </div>
\ No newline at end of file diff --git a/sonar-server/src/main/webapp/WEB-INF/app/views/project/settings/_plugins.html.erb b/sonar-server/src/main/webapp/WEB-INF/app/views/project/settings/_plugins.html.erb deleted file mode 100644 index 7582ee2d3d4..00000000000 --- a/sonar-server/src/main/webapp/WEB-INF/app/views/project/settings/_plugins.html.erb +++ /dev/null @@ -1,4 +0,0 @@ -<h1 class="marginbottom10">Settings</h1> -<div class="yui-g widget" id="widget_plugins"> -<%= render :partial => 'settings/plugins', :locals => {:project=>@project} %> -</div>
\ No newline at end of file diff --git a/sonar-server/src/main/webapp/WEB-INF/app/views/resource/extension.html.erb b/sonar-server/src/main/webapp/WEB-INF/app/views/resource/extension.html.erb index b61e6259fc4..c5873493d32 100644 --- a/sonar-server/src/main/webapp/WEB-INF/app/views/resource/extension.html.erb +++ b/sonar-server/src/main/webapp/WEB-INF/app/views/resource/extension.html.erb @@ -18,6 +18,6 @@ <% end %> -<% else # ruby on rails page %> - <%= render :inline => @page.getTemplate() %> +<% elsif @extension.getTarget() # ruby on rails page %> + <%= render :inline => @extension.getTarget().getTemplate() -%> <% end %>
\ No newline at end of file diff --git a/sonar-server/src/main/webapp/WEB-INF/app/views/reviews/_assign_form.html.erb b/sonar-server/src/main/webapp/WEB-INF/app/views/reviews/_assign_form.html.erb index df4a07ab164..06cf4dadea1 100644 --- a/sonar-server/src/main/webapp/WEB-INF/app/views/reviews/_assign_form.html.erb +++ b/sonar-server/src/main/webapp/WEB-INF/app/views/reviews/_assign_form.html.erb @@ -1,11 +1,16 @@ +<% + assignee_check_script = "if ($('autocompleteText-assignee_login').value != '' && $('assignee_login').value == '') { alert($('autocompleteText-assignee_login').value + '" + message('reviews.user_does_not_exist') + "'); return false;}" +%> + <form method="post" - onsubmit="if ($('assignee_login').value != '') { new Ajax.Updater('review', '<%= url_for :action => 'assign' -%>', {asynchronous:true, evalScripts:true, parameters:Form.serialize(this)}); };return false;"> + onsubmit="<%= assignee_check_script -%> new Ajax.Updater('review', '<%= url_for :action => 'assign' -%>', {asynchronous:true, evalScripts:true, parameters:Form.serialize(this)}); return false;"> <%= hidden_field_tag :id, params[:review_id] -%> <%= user_autocomplete_field "assignee_login", "" -%> <%= submit_to_remote "submit_btn", message('assign'), :url => { :action => 'assign' }, - :update => "review" -%> + :update => "review", + :before => assignee_check_script -%> <%= link_to_remote message('cancel'), :url => { :action => 'show', :id => params[:review_id] }, diff --git a/sonar-server/src/main/webapp/WEB-INF/app/views/reviews/_violation_assign_form.html.erb b/sonar-server/src/main/webapp/WEB-INF/app/views/reviews/_violation_assign_form.html.erb index 5977b875884..3435a2f3da0 100644 --- a/sonar-server/src/main/webapp/WEB-INF/app/views/reviews/_violation_assign_form.html.erb +++ b/sonar-server/src/main/webapp/WEB-INF/app/views/reviews/_violation_assign_form.html.erb @@ -1,13 +1,18 @@ +<% + assignee_check_script = "if ($('autocompleteText-assignee_login').value != '' && $('assignee_login').value == '') { alert($('autocompleteText-assignee_login').value + '" + message('reviews.user_does_not_exist') + "'); return false;}" +%> + <%= image_tag("sep12.png") -%> <form method="post" - onsubmit="if ($('assignee_login').value != '') { new Ajax.Updater('vId<%= params[:violation_id] -%>', '<%= url_for :action => 'violation_assign' -%>', {asynchronous:true, evalScripts:true, parameters:Form.serialize(this)}); };return false;"> + onsubmit="<%= assignee_check_script -%> new Ajax.Updater('vId<%= params[:violation_id] -%>', '<%= url_for :action => 'violation_assign' -%>', {asynchronous:true, evalScripts:true, parameters:Form.serialize(this)}); return false;"> <%= hidden_field_tag :id, params[:violation_id] -%> <%= user_autocomplete_field "assignee_login", "" -%> <%= submit_to_remote "submit_btn", message('assign'), :url => { :action => 'violation_assign' }, - :update => "vId" + params[:violation_id] -%> + :update => "vId" + params[:violation_id], + :before => assignee_check_script -%> <%= link_to_remote message('cancel'), :url => { :action => 'display_violation', :id => params[:violation_id] }, diff --git a/sonar-server/src/main/webapp/WEB-INF/app/views/reviews/_violation_comment_form.html.erb b/sonar-server/src/main/webapp/WEB-INF/app/views/reviews/_violation_comment_form.html.erb index 20752d630fe..b9c5db8e5b4 100644 --- a/sonar-server/src/main/webapp/WEB-INF/app/views/reviews/_violation_comment_form.html.erb +++ b/sonar-server/src/main/webapp/WEB-INF/app/views/reviews/_violation_comment_form.html.erb @@ -1,8 +1,13 @@ <% button=(@comment ? message('update_comment') : message('add_comment')) + + if @violation.review.nil? || @violation.review.comments.size==0 + # This is the first comment, we must ask for the assignee + assignee_check_script = "if ($('autocompleteText-assignee_login').value != '' && $('assignee_login').value == '') { alert($('autocompleteText-assignee_login').value + '" + message('reviews.user_does_not_exist') + "'); return false;}" + end %> <form method="POST" - onsubmit="new Ajax.Updater('vId<%= params[:id] -%>', '<%= url_for :action => 'violation_save_comment' -%>', {asynchronous:true, evalScripts:true, parameters:Form.serialize(this)});return false;"> + onsubmit="<%= assignee_check_script if assignee_check_script -%> new Ajax.Updater('vId<%= params[:id] -%>', '<%= url_for :action => 'violation_save_comment' -%>', {asynchronous:true, evalScripts:true, parameters:Form.serialize(this)});return false;"> <input type="hidden" name="id" value="<%= params[:id] -%>"/> <% if @comment %> <input type="hidden" name="comment_id" value="<%= @comment.id -%>"/> @@ -16,7 +21,8 @@ <%= submit_to_remote "submit_btn"+params[:id] , button, :url => { :action => 'violation_save_comment'}, :html => { :id => "submit_btn"+params[:id], :disabled => "true" }, - :update => 'vId'+params[:id] -%> + :update => 'vId'+params[:id], + :before => assignee_check_script ? assignee_check_script : "" -%> <%= link_to_remote message('cancel'), :url => {:action => 'display_violation', :id => params[:id]}, :update => 'vId' + params[:id] -%> diff --git a/sonar-server/src/main/webapp/WEB-INF/app/views/rules/show.html.erb b/sonar-server/src/main/webapp/WEB-INF/app/views/rules/show.html.erb index c4b783205b8..c7850424fdf 100644 --- a/sonar-server/src/main/webapp/WEB-INF/app/views/rules/show.html.erb +++ b/sonar-server/src/main/webapp/WEB-INF/app/views/rules/show.html.erb @@ -4,21 +4,4 @@ Plugin: <%= @rule.plugin_name -%> Key: <%= @rule.plugin_rule_key %></div> <div class="doc"> <p><%= @rule.description %></p> -</div> - -<% unless @rule.parameters.empty? %> -<table> - <thead> - <tr><th colspan="3"><b>Parameters</b></th></tr> - </thead> - <tbody> - <% @rule.parameters.each do |parameter| %> - <tr> - <td><code><%= parameter.name -%>:</code></td> - <td class="sep"> </td> - <td><Value><%= parameter.description -%></td> - </tr> - <% end %> - </tbody> -</table> -<% end %>
\ No newline at end of file +</div>
\ No newline at end of file diff --git a/sonar-server/src/main/webapp/WEB-INF/app/views/server_id_configuration/index.html.erb b/sonar-server/src/main/webapp/WEB-INF/app/views/server_id_configuration/index.html.erb new file mode 100644 index 00000000000..541f6d71ed6 --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/app/views/server_id_configuration/index.html.erb @@ -0,0 +1,60 @@ +<h3 class="marginbottom10"><%= message('server_id_configuration.page') -%></h3> + +<% if @server_id %> + <p> + <big><b><span class="<%= @bad_id ? 'error' : 'notice' -%>" id="server_id"><%= @server_id -%></span></b></big> + <% if @bad_id %> + <span class="error"><%= message('server_id_configuration.bad_id') -%></span> + <% end %> + </p> + <br/> +<% end %> + +<p> + <%= message('server_id_configuration.information') -%> +</p> + +<% form_tag :action => 'generate' do %> + <table class="data marginbottom10"> + <thead> + <tr> + <th></th> + </tr> + </thead> + <tfoot> + <tr> + <td colspan="3"> + <%= submit_tag message('server_id_configuration.generate_button'), :disable_with => message('server_id_configuration.generating_button'), :id => 'generate-button' -%> + </td> + </tr> + </tfoot> + <tbody> + <tr class="even"> + <td style="padding: 10px"> + <h3><%= message('server_id_configuration.organisation.title') -%></h3> + + <p class="marginbottom10"><%= message('server_id_configuration.organisation.desc') -%></p> + + <p> + <input type="text" name="organisation" value="<%= @organisation -%>"/> + </p> + </td> + </tr> + <tr class="odd"> + <td style="padding: 10px"> + <h3><%= message('server_id_configuration.ip.title') -%></h3> + + <p class="marginbottom10"><%= message('server_id_configuration.ip.desc') -%></p> + <ul class="marginbottom10 bullet"> + <% @valid_addresses.each_with_index do |ip_address, index| %> + <li><span id="address_<%= index -%>"><%= ip_address.getHostAddress() -%></span></li> + <% end %> + </ul> + <p> + <input type="text" name="address" value="<%= @address -%>"/> + </p> + </td> + </tr> + </tbody> + </table> +<% end %>
\ No newline at end of file diff --git a/sonar-server/src/main/webapp/WEB-INF/app/views/settings/_plugins.html.erb b/sonar-server/src/main/webapp/WEB-INF/app/views/settings/_plugins.html.erb index d0d7ad83c44..ec7db02725e 100644 --- a/sonar-server/src/main/webapp/WEB-INF/app/views/settings/_plugins.html.erb +++ b/sonar-server/src/main/webapp/WEB-INF/app/views/settings/_plugins.html.erb @@ -1,130 +1,133 @@ <style type="text/css"> -#plugins .plugin { + #plugins .plugin { padding: 5px; border: 1px solid #ddd; background-color: #fff; - } -#plugins .plugin h2 { - margin-left: 10px; - font-size: 122%; - color: #333; -} -#plugins .plugin h3 { - margin-left: 5px; -} -#plugins .plugin p { - padding: 5px 5px; - } -#plugins .plugin img { - padding: 5px 0 0 5px; - } -</style> -<div id="plugins"> - <% form_tag :controller => :settings, :action => :update do - plugins = controller.java_facade.getPluginsMetadata().sort {|p1,p2| p1.getName() <=> p2.getName()} - plugin_properties={} - plugins.each do |plugin| - properties = controller.java_facade.getPluginProperties(plugin).select do |property| - (project && project.module? && property.module()) or (project && project.project? && property.project()) or (project.nil? && property.global) - end - plugin_properties[plugin]=properties - end - - %> - <% if project %> - <input type="hidden" name="resource_id" value="<%= project.id -%>" ></input> - <% end %> - <table width="100%"> - <tr> - <td width="1%" nowrap class="column first"> - <table class="data selector"> - <thead><tr><th> - <span>Select plugin</span> - </th></tr></thead> - <tbody> - <% plugins.each do |plugin| - if plugin_properties[plugin].empty? - # we display only plugins with properties - next - end - %> - <tr class="select <%= cycle('even', 'odd', :name => 'plugins') -%>" id="select_<%= plugin.getKey() -%>"> - <td><a href="#" onclick="showPlugin('<%= plugin.getKey() -%>')"><%= h(plugin.getName()) -%></a></td> - </tr> - <% end %> - </tbody> - </table> - <br/> - <%= submit_tag('Save parameters') %> - </td> - <td class="column"> - - <% - values = Property.hash(project ? project.id : nil) - plugins.each do |plugin| - properties = plugin_properties[plugin] - %> - <div class="plugin" id="plugin_<%= plugin.getKey() -%>" style="display: none;"> - <table class="spaced"> - <% unless plugin.getDescription().blank? %> - <tr><td class="odd"> - <p><%= plugin.getDescription() %></p> - </td></tr> - <% end %> + } + #plugins .plugin h2 { + margin-left: 10px; + font-size: 122%; + color: #333; + } - <% properties.each do |property| - value= values[property.key()] || '' - css = cycle('even', 'odd', :name => plugin.getKey()) - %> - <tr class="<%= css -%>"> - <td> - <h3><%= property.name() %> <% if property.project() %><span class="note">[<%= property.key() -%>]</span><% end %></h3> - <p><%= property.description() %></p> - <p> - <% span_id = "text_" + property.key().gsub('.', '_') %> - <% textfield = text_field_tag property.key(), value, :size => '20' %> - <% textfield += link_to_function(image_tag("zoom.png"), "replaceTextField('#{span_id}', '#{property.key()}')", :id => "toggle_text", :class => 'nolink') %> - <% textarea = text_area_tag property.key(), value, :size => "100x10" %> - <span id="<%= span_id %>"><%= (value.length < 50) ? textfield : textarea %></span> + #plugins .plugin h3 { + margin-left: 5px; + } - <% unless property.defaultValue().blank? %> - <% if project.nil? %> - <span class="note">Default : <%= h property.defaultValue() -%></span> - <% else %> - <span class="note">Default : <%= h Property.value(property.key(), nil, property.defaultValue()) -%></span> - <% end %> - <% end %> - </p> - </td> - </tr> - - <% end %> - </table> - </div> - <% end %> - </td> - </tr> - </table> - <% end %> -</div> + #plugins .plugin p { + padding: 5px 5px; + } + #plugins .plugin img { + padding: 5px 0 0 5px; + } +</style> <script type="text/javascript"> function replaceTextField(span_id, key) { var text_field_value = $F(key); var text_area = '<textarea cols="100" id="' + key + '" name="' + key + '" rows="10">' + text_field_value + '</textarea>'; $(span_id).replace(text_area); } - function showPlugin(id) { - $$('.plugin').each(function(element) { - element.hide(); - }); - $$('.select').each(function(element) { - element.removeClassName('selected'); - }); - $('plugin_' + id).show(); - $('select_' + id).addClassName('selected'); - return false; - } - showPlugin('core'); -</script>
\ No newline at end of file +</script> +<div id="plugins"> + <table width="100%"> + <tr> + <td width="1%" nowrap class="column first"> + <table class="data selector"> + <thead> + <tr> + <th> + <span>Category</span> + </th> + </tr> + </thead> + <tbody> + <% + @properties_per_category.keys.sort_by{|category| message("property.category.#{category}", :default => category).upcase}.each do |category| + if !@properties_per_category[category].empty? || SettingsController::SPECIAL_CATEGORIES.include?(category) + %> + <tr class="select <%= cycle('even', 'odd', :name => 'category') -%> <%= 'selected' if @category==category -%>" id="select_<%= category -%>"> + <td><%= link_to message("property.category.#{category}", :default => category), :overwrite_params => {:category => category} -%></td> + </tr> + <% end + end + %> + </tbody> + </table> + <br/> + </td> + + <td class="column"> + <% if @category && @properties_per_category[@category] + category_name = message("property.category.#{@category}", :default => @category) + if SettingsController::SPECIAL_CATEGORIES.include?(@category) + %> + <%= render :partial => 'special', :locals => {:url => url_for(:controller => "#{@category}_configuration")} -%> + <% + elsif !@properties_per_category[@category].empty? + %> + <% form_tag :controller => :settings, :action => :update do %> + <%= hidden_field_tag('category', @category) -%> + <% if @project %> + <input type="hidden" name="resource_id" value="<%= @project.id -%>"/> + <% end %> + <table class="data marginbottom10"> + <thead> + <tr> + <th> + <span><%= h(category_name) -%></span> + </th> + </tr> + </thead> + <tbody> + <% + if @properties_per_category[@category] + @properties_per_category[@category].each do |property| + value = Property.value(property.key(), (@project ? @project.id : nil), '') + %> + <tr class="<%= cycle('even', 'odd', :name => 'properties') -%>"> + <td style="padding: 10px"> + <h3> + <%= message("property.#{property.key()}.name", :default => property.name()) -%> + <% if property.project() %> + <br/><span class="note"><%= property.key() -%></span> + <% end %> + </h3> + <% + desc=message("property.#{property.key()}.description", :default => property.description()) + if desc.present? %> + <p class="marginbottom10"><%= desc -%></p> + <% end %> + <p> + <% span_id = "text_" + property.key().gsub('.', '_') %> + <% textfield = text_field_tag property.key(), value, :size => '20' %> + <% textfield += link_to_function(image_tag("zoom.png"), "replaceTextField('#{span_id}', '#{property.key()}')", :id => "toggle_text", :class => 'nolink') %> + <% textarea = text_area_tag property.key(), value, :size => "100x10" %> + <span id="<%= span_id %>"><%= (value.length < 50) ? textfield : textarea %></span> + + <% unless property.defaultValue().blank? %> + <% if @project %> + <span class="note">Default : <%= h Property.value(property.key(), nil, property.defaultValue()) -%></span> + <% else %> + <span class="note">Default : <%= h property.defaultValue() -%></span> + <% end %> + <% end %> + </p> + </td> + </tr> + <% end + end + %> + </tbody> + </table> + <% save_message=message('settings.save_category', :params => [category_name]) %> + <%= submit_tag(save_message, :disable_with => save_message, :id => 'save') -%> + <% end %> + <% end + end + %> + </td> + </tr> + </table> +</div> diff --git a/sonar-server/src/main/webapp/WEB-INF/app/views/settings/_special.html.erb b/sonar-server/src/main/webapp/WEB-INF/app/views/settings/_special.html.erb new file mode 100644 index 00000000000..9b926826500 --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/app/views/settings/_special.html.erb @@ -0,0 +1 @@ +<iframe src="<%= url -%>" width="100%" height="600" frameborder="0" scrolling="no" name="settings_iframe"></iframe>
\ No newline at end of file diff --git a/sonar-server/src/main/webapp/WEB-INF/app/views/system/index.html.erb b/sonar-server/src/main/webapp/WEB-INF/app/views/system/index.html.erb index 8d99bb71e55..f946a8d84a6 100644 --- a/sonar-server/src/main/webapp/WEB-INF/app/views/system/index.html.erb +++ b/sonar-server/src/main/webapp/WEB-INF/app/views/system/index.html.erb @@ -4,73 +4,74 @@ </li> </ul> -<table class="data width100" id="system_info"> +<table class="data width100" id="sonar"> <thead> <tr> - <th colspan="2"><h2>System Info</h2></th> + <th colspan="2"><h2>Sonar Info</h2></th> </tr> </thead> <tbody> - <% @server.system_info.each do |data| %> - <%= render :partial => 'row', :locals => {:title => data[0], :value => data[1], :name => 'system' } %> + <% @server.sonar_info.each do |data| %> + <%= render :partial => 'row', :locals => {:title => data[0], :value => data[1], :name => 'sonar' } %> <% end %> <tbody> </table> <br/> -<table class="data width100" id="sonar"> +<table class="data width100" id="plugins"> <thead> <tr> - <th colspan="2"><h2>Sonar Info</h2></th> + <th colspan="2"><h2>Sonar Plugins</h2></th> </tr> </thead> <tbody> - <% @server.sonar_info.each do |data| %> - <%= render :partial => 'row', :locals => {:title => data[0], :value => data[1], :name => 'sonar' } %> - <% end %> + <% + user_plugins=@server.sonar_plugins + if user_plugins.empty? + %> + <tr><td colspan="2" class="even">None</td></tr> + <% else %> + <% user_plugins.each do |data| %> + <%= render :partial => 'row', :locals => {:title => data[0], :value => data[1], :name => 'plugins' } %> + <% end %> + <% end %> <tbody> </table> <br/> -<table class="data width100" id="memory"> +<table class="data width100" id="system_info"> <thead> <tr> - <th colspan="2"><h2>Java VM Statistics</h2></th> + <th colspan="2"><h2>System Info</h2></th> </tr> </thead> <tbody> - <% @server.system_statistics.each do |data| %> - <%= render :partial => 'row', :locals => {:title => data[0], :value => data[1], :name => 'memory' } %> + <% @server.system_info.each do |data| %> + <%= render :partial => 'row', :locals => {:title => data[0], :value => data[1], :name => 'system' } %> <% end %> <tbody> </table> <br/> -<table class="data width100" id="plugins"> +<table class="data width100" id="memory"> <thead> <tr> - <th colspan="2"><h2>Sonar Plugins</h2></th> + <th colspan="2"><h2>Java VM Statistics</h2></th> </tr> </thead> <tbody> - <% - user_plugins=@server.sonar_plugins - if user_plugins.empty? - %> - <tr><td colspan="2" class="even">None</td></tr> - <% else %> - <% user_plugins.each do |data| %> - <%= render :partial => 'row', :locals => {:title => data[0], :value => data[1], :name => 'plugins' } %> - <% end %> - <% end %> + <% @server.system_statistics.each do |data| %> + <%= render :partial => 'row', :locals => {:title => data[0], :value => data[1], :name => 'memory' } %> + <% end %> <tbody> </table> <br/> + <table class="data width100" id="system_properties"> <thead> <tr> diff --git a/sonar-server/src/main/webapp/WEB-INF/app/views/timemachine/index.html.erb b/sonar-server/src/main/webapp/WEB-INF/app/views/timemachine/index.html.erb index a641423752c..5aada6364a2 100644 --- a/sonar-server/src/main/webapp/WEB-INF/app/views/timemachine/index.html.erb +++ b/sonar-server/src/main/webapp/WEB-INF/app/views/timemachine/index.html.erb @@ -1,3 +1,6 @@ +<%= javascript_include_tag 'calendar/yahoo-dom-event-min.js' -%> +<%= javascript_include_tag 'calendar/calendar-min.js' -%> + <div id="timemachine"> <style type="text/css"> #calContainer { display:none; position:absolute; } diff --git a/sonar-server/src/main/webapp/WEB-INF/app/views/users/index.html.erb b/sonar-server/src/main/webapp/WEB-INF/app/views/users/index.html.erb index f7ea9750355..15fad13ebab 100644 --- a/sonar-server/src/main/webapp/WEB-INF/app/views/users/index.html.erb +++ b/sonar-server/src/main/webapp/WEB-INF/app/views/users/index.html.erb @@ -24,6 +24,7 @@ </td> <td class="left" valign="top"> <%= link_to "edit", { :id => user.id, :action => 'edit'}, :id => "edit-#{u user.login}" %> | + <%= link_to "change password", { :id => user.id, :action => 'change_password'}, :id => "change-password-#{u user.login}" %> | <%= link_to "delete", {:action => 'destroy', :id => user.id}, {:confirm => "Warning : are you sure to delete this user ?", :method => 'delete', :id => "delete-#{u user.login}"} %> </td> </tr> @@ -34,15 +35,19 @@ </td> <td class="sep"> </td> <td valign="top" align="right" width="210"> - <% - action_name = 'create' - title='Add new user' - if @user.id - action_name = 'update' - title='Edit user' + <% + action_name = 'create' + title = 'Add new user' + if @user.id + action_name = 'update' + title = 'Edit user' + if params[:action] == 'change_password' + action_name = 'update_password' + title = 'Change password' end - %> - <% form_for :user, @user, :url => { :id => @user.id, :action => action_name}, :html => { :method => @user.id.nil?? :post : :put } do |f| %> + end + %> + <% form_for :user, @user, :url => { :id => @user.id, :action => action_name}, :html => { :method => @user.id.nil?? :post : :put } do |f| %> <table class="admintable" width="100%"> <tr> <td class="left" valign="top"><h1><%= title %></h1></td> @@ -52,34 +57,42 @@ <% if @user.id %> <%= @user.login %> <%= f.hidden_field :login %> - <% else %> <br/><%= f.text_field :login, :size => 30, :maxLength => 40 %> <% end %> </td> - </tr> + <% if params[:action] != 'change_password' %> <tr> <td class="left" valign="top">Name:<br/><%= f.text_field :name, :size => 30, :maxLength => 200 %></td> </tr> <tr> <td class="left" valign="top">Email:<br/><%= f.text_field :email, :size => 30, :maxLength => 100 %></td> </tr> + <% end %> + <% if !@user.id %> <tr> <td class="left" valign="top">Password:<br/><%= f.password_field :password, :size => 30, :maxLength => 50 %></td> </tr> <tr> <td class="left" valign="top">Confirm password:<br/><%= f.password_field :password_confirmation, :size => 30, :maxLength => 50 %></td> - </tr> + </tr> + <% elsif params[:action] == 'change_password' %> + <tr> + <td class="left" valign="top">New password:<br/><%= f.password_field :password, :size => 30, :maxLength => 50 %></td> + </tr> + <tr> + <td class="left" valign="top">Confirm new password:<br/><%= f.password_field :password_confirmation, :size => 30, :maxLength => 50 %></td> + </tr> + <% end %> <tr> <td class="left" nowrap="nowrap" valign="top" colspan="2"> <%= submit_tag @user.id.nil?? 'Create':'Update' %> <%= link_to 'cancel', { :controller => 'users', :action => 'index'}, { :class => 'action' } %><br/> </td> </tr> - </table> - <% end %> + <% end %> </td> </tr> -</table>
\ No newline at end of file +</table> diff --git a/sonar-server/src/main/webapp/WEB-INF/config/database.yml b/sonar-server/src/main/webapp/WEB-INF/config/database.yml index 4dd0ad9ca87..fe16fdbd96e 100644 --- a/sonar-server/src/main/webapp/WEB-INF/config/database.yml +++ b/sonar-server/src/main/webapp/WEB-INF/config/database.yml @@ -1,6 +1,30 @@ -# JDBC configured is loaded from Sonar +base: &base + adapter: <%= ::Java::OrgSonarServerUi::JRubyFacade.getInstance().getDialect().getActiveRecordJdbcAdapter() %> + username: <%= ::Java::OrgSonarServerUi::JRubyFacade.getInstance().getConfigurationValue('sonar.jdbc.username' ) || 'sonar' %> + password: <%= ::Java::OrgSonarServerUi::JRubyFacade.getInstance().getConfigurationValue('sonar.jdbc.password') || 'sonar' %> + url: <%= ::Java::OrgSonarServerUi::JRubyFacade.getInstance().getConfigurationValue('sonar.jdbc.url') %> + dialect: <%= ::Java::OrgSonarServerUi::JRubyFacade.getInstance().getDialect().getActiveRecordDialectCode() %> + driver: <%= ::Java::OrgSonarServerUi::JRubyFacade.getInstance().getConfigurationValue('sonar.jdbc.driverClassName') %> + pool: <%= ::Java::OrgSonarServerUi::JRubyFacade.getInstance().getConfigurationValue('sonar.jdbc.maxActive') || 10 %> + + # PostgreSQL +<% + search_path = ::Java::OrgSonarServerUi::JRubyFacade.getInstance().getConfigurationValue('sonar.jdbc.postgreSearchPath') + if search_path +%> + schema_search_path: <%= search_path %> +<% end %> + # Oracle +<% + schema = ::Java::OrgSonarServerUi::JRubyFacade.getInstance().getConfigurationValue('sonar.hibernate.default_schema') + if schema +%> + schema: <%= schema %> +<% + end +%> development: - adapter: jdbc - + <<: *base + production: - adapter: jdbc
\ No newline at end of file + <<: *base
\ No newline at end of file diff --git a/sonar-server/src/main/webapp/WEB-INF/config/environment.rb b/sonar-server/src/main/webapp/WEB-INF/config/environment.rb index 2aaca86abb7..ed28def4eae 100644 --- a/sonar-server/src/main/webapp/WEB-INF/config/environment.rb +++ b/sonar-server/src/main/webapp/WEB-INF/config/environment.rb @@ -53,36 +53,6 @@ Rails::Initializer.run do |config| # config.active_record.observers = :cacher, :garbage_collector, :forum_observer end - -module ActiveRecord - module ConnectionAdapters - - # Patch to delegate configuration of JDBC datasource to Sonar. - # See vendor/gems/activerecord-jdbc-adapter/lib/active_record/connection_adapters/jdbc_adapter.rb - class JdbcConnection - def initialize(config) - @config = config.symbolize_keys! - @config[:retry_count] ||= 5 - @config[:connection_alive_sql] ||= ::Java::OrgSonarServerUi::JRubyFacade.getInstance().getConfigurationValue('sonar.jdbc.validationQuery') - - @jndi_connection = true # used in JndiConnectionPoolCallbacks to close this initial connection - - @connection_factory = JdbcConnectionFactory.impl do - ::Java::OrgSonarServerUi::JRubyFacade.getInstance().getConnection() - end - @config[:dialect] = ::Java::OrgSonarServerUi::JRubyFacade.getInstance().getDialect().getActiveRecordDialectCode() - - connection # force the connection to load - set_native_database_types - @stmts = {} - rescue Exception => e - raise "Fail to connect to database: #{e}" - end - end - end -end - - class ActiveRecord::Migration def self.alter_to_big_primary_key(tablename) dialect = ::Java::OrgSonarServerUi::JRubyFacade.getInstance().getDialect().getActiveRecordDialectCode() @@ -116,113 +86,6 @@ class ActiveRecord::Migration end end -module JdbcSpec - - # - # Ticket http://tools.assembla.com/sonar/ticket/200 - # Problem with mysql TEXT columns. ActiveRecord :text type is mapped to TEXT type (65535 characters). - # But we would like the bigger MEDIUMTEXT for the snapshot_sources table (16777215 characters). - # This hack works only for ActiveRecord-JDBC (Jruby use). - # See http://www.headius.com/jrubywiki/index.php/Adding_Datatypes_to_ActiveRecord-JDBC - # The following has been copied from WEB-INF\gems\gems\activerecord-jdbc-adapter-0.9\lib\jdbc_adapter\jdbc_mysql.rb - # Problem still in activerecord-jdbc-adapter 0.9 - module MySQL - def modify_types(tp) - tp[:primary_key] = "int(11) DEFAULT NULL auto_increment PRIMARY KEY" - tp[:decimal] = { :name => "decimal" } - tp[:timestamp] = { :name => "datetime" } - tp[:datetime][:limit] = nil - - # sonar - tp[:text] = { :name => "mediumtext" } - tp[:binary] = { :name => "longblob" } - tp[:big_integer] = { :name => "bigint"} - - tp - end - end - - # wrong column types on oracle 10g timestamp and datetimes - # Problem still in activerecord-jdbc-adapter 0.8 - module Oracle - def modify_types(tp) - tp[:primary_key] = "NUMBER(38) NOT NULL PRIMARY KEY" - tp[:integer] = { :name => "NUMBER", :limit => 38 } - tp[:datetime] = { :name => "TIMESTAMP" } # updated for sonar - tp[:timestamp] = { :name => "TIMESTAMP" } # updated for sonar - tp[:time] = { :name => "DATE" } - tp[:date] = { :name => "DATE" } - - #sonar - tp[:big_integer] = { :name => "NUMBER", :limit => 38 } - - tp - end - - end - - module MsSQL - def modify_types(tp) - tp[:primary_key] = "int NOT NULL IDENTITY(1, 1) PRIMARY KEY" - tp[:integer][:limit] = nil - tp[:boolean] = {:name => "bit"} - tp[:binary] = { :name => "image"} - - # sonar patch: - tp[:text] = { :name => "NVARCHAR(MAX)" } - tp[:big_integer] = { :name => "bigint"} - end - - end - - # activerecord-jdbc-adapter has a missing quote_table_name method - module Derby - def modify_types(tp) - tp[:primary_key] = "int generated by default as identity NOT NULL PRIMARY KEY" - tp[:integer][:limit] = nil - tp[:string][:limit] = 256 - tp[:boolean] = {:name => "smallint"} - - #sonar - tp[:big_integer] = {:name => "bigint"} - - tp - end - - def quote_table_name(name) #:nodoc: - quote_column_name(name).gsub('.', '`.`') - end - end - - module PostgreSQL - def modify_types(tp) - tp[:primary_key] = "serial primary key" - tp[:integer][:limit] = nil - tp[:boolean][:limit] = nil - - # sonar - # tp[:string][:limit] = 255 - tp[:big_integer] = { :name => "int8", :limit => nil } - - tp - end - - # See SONAR-862 on Postgre search_path setting. - # The issue is fixed in next activerecord-jdbc-adapter version: http://github.com/nicksieger/activerecord-jdbc-adapter/commit/2575700d3aee2eb395cac3e7933bb4d129fa2f03 - # More details on https://rails.lighthouseapp.com/projects/8994/tickets/918-postgresql-tables-not-generating-correct-schema-list - def columns(table_name, name=nil) - # schema_name must be nil instead of "public" - schema_name = nil - if table_name =~ /\./ - parts = table_name.split(/\./) - table_name = parts.pop - schema_name = parts.join(".") - end - @connection.columns_internal(table_name, name, schema_name) - end - end -end - # patch for SONAR-1182. GWT does not support ISO8601 dates that end with 'Z' # http://google-web-toolkit.googlecode.com/svn/javadoc/1.6/com/google/gwt/i18n/client/DateTimeFormat.html module ActiveSupport @@ -243,3 +106,12 @@ end require File.dirname(__FILE__) + '/../lib/sonar_webservice_plugins.rb' require File.dirname(__FILE__) + '/../lib/database_version.rb' DatabaseVersion.automatic_setup + + +# +# +# IMPORTANT NOTE +# Some changes have been done in activerecord-jdbc-adapter. Most of them relate to column types. +# All these changes are prefixed by the comment #sonar +# +#
\ No newline at end of file diff --git a/sonar-server/src/main/webapp/WEB-INF/config/locales/en.yml b/sonar-server/src/main/webapp/WEB-INF/config/locales/en.yml new file mode 100644 index 00000000000..6ec7cd8e72a --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/config/locales/en.yml @@ -0,0 +1,180 @@ +# GB English translations for Ruby on Rails + +"en": + date: + formats: + default: "%d %b %Y" + short: "%d %b" + long: "%d %B, %Y" + + day_names: [Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday] + abbr_day_names: [Sun, Mon, Tue, Wed, Thu, Fri, Sat] + + month_names: [~, January, February, March, April, May, June, July, August, September, October, November, December] + abbr_month_names: [~, Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec] + order: + - :day + - :month + - :year + + time: + formats: + default: "%d %b %Y %H:%M" + short: "%d %b %H:%M" + long: "%d %B, %Y %H:%M" + am: "am" + pm: "pm" + + support: + array: + words_connector: ", " + two_words_connector: " and " + last_word_connector: ", and " + + select: + prompt: "Please select" + + number: + format: + separator: "." + delimiter: "," + precision: 3 + significant: false + strip_insignificant_zeros: false + + currency: + format: + format: "%u%n" + unit: "£" + separator: "." + delimiter: "," + precision: 2 + significant: false + strip_insignificant_zeros: false + + percentage: + format: + delimiter: "" + + precision: + format: + delimiter: "" + + human: + format: + delimiter: "" + precision: 3 + significant: true + strip_insignificant_zeros: true + storage_units: + format: "%n %u" + units: + byte: + one: "Byte" + other: "Bytes" + kb: "KB" + mb: "MB" + gb: "GB" + tb: "TB" + decimal_units: + format: "%n %u" + units: + unit: "" + thousand: Thousand + million: Million + billion: Billion + trillion: Trillion + quadrillion: Quadrillion + + datetime: + distance_in_words: + half_a_minute: "half a minute" + less_than_x_seconds: + one: "less than 1 second" + other: "less than %{count} seconds" + x_seconds: + one: "1 second" + other: "%{count} seconds" + less_than_x_minutes: + one: "less than a minute" + other: "less than %{count} minutes" + x_minutes: + one: "1 minute" + other: "%{count} minutes" + about_x_hours: + one: "about 1 hour" + other: "about %{count} hours" + x_days: + one: "1 day" + other: "%{count} days" + about_x_months: + one: "about 1 month" + other: "about %{count} months" + x_months: + one: "1 month" + other: "%{count} months" + about_x_years: + one: "about 1 year" + other: "about %{count} years" + over_x_years: + one: "over 1 year" + other: "over %{count} years" + almost_x_years: + one: "almost 1 year" + other: "almost %{count} years" + prompts: + year: "Year" + month: "Month" + day: "Day" + hour: "Hour" + minute: "Minute" + second: "Seconds" + + helpers: + select: + prompt: "Please select" + + submit: + create: 'Create %{model}' + update: 'Update %{model}' + submit: 'Save %{model}' + + errors: + format: "%{attribute} %{message}" + + messages: &errors_messages + inclusion: "is not included in the list" + exclusion: "is reserved" + invalid: "is invalid" + confirmation: "doesn't match confirmation" + accepted: "must be accepted" + empty: "can't be empty" + blank: "can't be blank" + too_long: "is too long (maximum is %{count} characters)" + too_short: "is too short (minimum is %{count} characters)" + wrong_length: "is the wrong length (should be %{count} characters)" + not_a_number: "is not a number" + not_an_integer: "must be an integer" + greater_than: "must be greater than %{count}" + greater_than_or_equal_to: "must be greater than or equal to %{count}" + equal_to: "must be equal to %{count}" + less_than: "must be less than %{count}" + less_than_or_equal_to: "must be less than or equal to %{count}" + odd: "must be odd" + even: "must be even" + + activerecord: + errors: + template: + header: + one: "1 error prohibited this %{model} from being saved" + other: "%{count} errors prohibited this %{model} from being saved" + body: "There were problems with the following fields:" + + messages: + taken: "has already been taken" + record_invalid: "Validation failed: %{errors}" + <<: *errors_messages + + full_messages: + format: "%{attribute} %{message}" diff --git a/sonar-server/src/main/webapp/WEB-INF/config/routes.rb b/sonar-server/src/main/webapp/WEB-INF/config/routes.rb index e0e0afe2416..178dd80df2f 100644 --- a/sonar-server/src/main/webapp/WEB-INF/config/routes.rb +++ b/sonar-server/src/main/webapp/WEB-INF/config/routes.rb @@ -10,6 +10,7 @@ ActionController::Routing::Routes.draw do |map| map.namespace :api do |api| api.resources :events, :only => [:index, :show, :create, :destroy] api.resources :user_properties, :only => [:index, :show, :create, :destroy], :requirements => { :id => /.*/ } + api.resources :projects, :only => [:index, :destroy], :requirements => { :id => /.*/ } api.resources :favorites, :only => [:index, :show, :create, :destroy], :requirements => { :id => /.*/ } api.resources :manual_measures, :only => [:index, :create, :destroy], :requirements => { :id => /.*/ } api.resources :reviews, :only => [:index, :show, :create], :member => { diff --git a/sonar-server/src/main/webapp/WEB-INF/db/migrate/001_initial_schema.rb b/sonar-server/src/main/webapp/WEB-INF/db/migrate/001_initial_schema.rb index 80976a2a65b..5eebd68d994 100644 --- a/sonar-server/src/main/webapp/WEB-INF/db/migrate/001_initial_schema.rb +++ b/sonar-server/src/main/webapp/WEB-INF/db/migrate/001_initial_schema.rb @@ -23,24 +23,46 @@ class InitialSchema < ActiveRecord::Migration t.column :name, :string, :null => true, :limit => 256 t.column :description, :string, :null => true, :limit => 2000 t.column :enabled, :boolean, :null => false, :default => true + t.column 'scope', :string, :limit => 3 + t.column 'qualifier', :string, :limit => 3 + t.column 'kee', :string, :limit => 400 + t.column 'root_id', :integer + t.column :profile_id, :integer, :null => true + t.column :language, :string, :null => true, :limit => 5 + t.column :copy_resource_id, :integer, :null => true + t.column :long_name, :string, :null => true, :limit => 256 end create_table :snapshots do |t| t.column :created_at, :datetime, :null => true, :default => nil - t.column :version, :string, :null => false, :limit => 32 t.column :project_id, :integer, :null => false t.column :parent_snapshot_id, :integer, :null => true t.column :status, :string, :null => false, :default => 'U', :limit => 4 - t.column :purged, :boolean, :null => false, :default => false t.column :islast, :boolean, :null => false, :default => false - + t.column 'scope', :string, :limit => 3 + t.column 'qualifier', :string, :limit => 3 + t.column 'root_snapshot_id', :integer + t.column 'version', :string, :limit => 60, :null => true + t.column :path, :string, :null => true, :limit => 96 + t.column :depth, :integer, :null => true + t.column :root_project_id, :integer, :nullable => true end create_table :metrics do |t| t.column :name, :string, :null => false, :limit => 64 - t.column :value_type, :integer, :null => false t.column :description, :string, :null => true, :limit => 255 t.column :direction, :integer, :null => false, :default => 0 + t.column :domain, :string, :null => true, :limit => 64 + t.column :short_name, :string, :null => true, :limit => 64 + t.column :qualitative, :boolean, :null => false, :default => false + t.column :val_type, :string, :null => true, :limit => 8 + t.column :user_managed, :boolean, :null => true, :default => false + t.column :enabled, :boolean, :null => true, :default => true + t.column :origin, :string, :null => true, :limit => 3 + t.column 'worst_value', :decimal, :null => true, :precision => 30, :scale => 20 + t.column 'best_value', :decimal, :null => true, :precision => 30, :scale => 20 + t.column 'optimized_best_value', :boolean, :null => true + t.column 'hidden', :boolean, :null => true end create_table :project_measures do |t| @@ -49,6 +71,18 @@ class InitialSchema < ActiveRecord::Migration t.column :snapshot_id, :integer, :null => true t.column :rule_id, :integer t.column :rules_category_id, :integer + t.column :text_value, :string, :limit => 96, :null => true + t.column 'tendency', :integer, :null => true + t.column :measure_date, :datetime, :null => true + t.column :project_id, :integer, :null => true + t.column :alert_status, :string, :limit => 5, :null => true + t.column :alert_text, :string, :null => true, :limit => 4000 + t.column :url, :string, :null => true, :limit => 2000 + t.column :description, :string, :null => true, :limit => 4000 + t.column :rule_priority, :integer, :null => true + t.column :diff_value_1, :decimal, :null => true, :precision => 30, :scale => 20 + t.column :diff_value_2, :decimal, :null => true, :precision => 30, :scale => 20 + t.column :diff_value_3, :decimal, :null => true, :precision => 30, :scale => 20 end create_table :rules_categories do |t| @@ -57,12 +91,13 @@ class InitialSchema < ActiveRecord::Migration end create_table :rules do |t| - t.column :name, :string, :null => false, :limit => 128 + t.column :name, :string, :null => false, :limit => 192 t.column :rules_category_id, :integer, :null => false t.column :plugin_rule_key, :string, :null => false, :limit => 200 t.column :plugin_config_key, :string, :null => false, :limit => 200 t.column :plugin_name, :string, :null => false, :limit => 255 t.column :description, :text + t.column :priority, :integer, :null => true end create_table :rule_failures do |t| @@ -70,6 +105,7 @@ class InitialSchema < ActiveRecord::Migration t.column :rule_id, :integer, :null => false t.column :failure_level, :integer, :null => false t.column :message, :string, :limit => 500 + t.column :line, :integer, :null => true end create_table :rules_parameters do |t| diff --git a/sonar-server/src/main/webapp/WEB-INF/db/migrate/002_index_database.rb b/sonar-server/src/main/webapp/WEB-INF/db/migrate/002_index_database.rb index 2e011054f24..bd4a96a58fc 100644 --- a/sonar-server/src/main/webapp/WEB-INF/db/migrate/002_index_database.rb +++ b/sonar-server/src/main/webapp/WEB-INF/db/migrate/002_index_database.rb @@ -27,6 +27,8 @@ class IndexDatabase < ActiveRecord::Migration add_index :rules_parameters, :rule_id, :name => 'rules_parameters_rule_id' add_index :snapshots, :project_id, :name => 'snapshot_project_id' + add_index :snapshots, :parent_snapshot_id, :name => 'snapshots_parent' + add_index :snapshots, :root_snapshot_id, :name => 'snapshots_root' add_index :metrics, :name, :unique => true, :name => 'metrics_unique_name' end diff --git a/sonar-server/src/main/webapp/WEB-INF/db/migrate/010_create_users.rb b/sonar-server/src/main/webapp/WEB-INF/db/migrate/010_create_users.rb index 635650bb47b..b41da2eca9a 100644 --- a/sonar-server/src/main/webapp/WEB-INF/db/migrate/010_create_users.rb +++ b/sonar-server/src/main/webapp/WEB-INF/db/migrate/010_create_users.rb @@ -1,45 +1,35 @@ - # - # Sonar, entreprise quality control tool. - # Copyright (C) 2008-2011 SonarSource - # mailto:contact AT sonarsource DOT com - # - # Sonar is free software; you can redistribute it and/or - # modify it under the terms of the GNU Lesser General Public - # License as published by the Free Software Foundation; either - # version 3 of the License, or (at your option) any later version. - # - # Sonar is distributed in the hope that it will be useful, - # but WITHOUT ANY WARRANTY; without even the implied warranty of - # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - # Lesser General Public License for more details. - # - # You should have received a copy of the GNU Lesser General Public - # License along with Sonar; if not, write to the Free Software - # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 - # +# +# Sonar, entreprise quality control tool. +# Copyright (C) 2008-2011 SonarSource +# mailto:contact AT sonarsource DOT com +# +# Sonar is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 3 of the License, or (at your option) any later version. +# +# Sonar is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with Sonar; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 +# class CreateUsers < ActiveRecord::Migration def self.up - create_users - end - - private - - def self.create_users create_table 'users' do |t| - t.column :login, :string, :limit => 40 - t.column :name, :string, :limit => 200, :null => true - t.column :email, :string, :limit => 100 - t.column :crypted_password, :string, :limit => 40 - t.column :salt, :string, :limit => 40 - t.column :created_at, :datetime - t.column :updated_at, :datetime - t.column :remember_token, :string, :limit => 500, :null => true + t.column :login, :string, :limit => 40 + t.column :name, :string, :limit => 200, :null => true + t.column :email, :string, :limit => 100 + t.column :crypted_password, :string, :limit => 40 + t.column :salt, :string, :limit => 40 + t.column :created_at, :datetime + t.column :updated_at, :datetime + t.column :remember_token, :string, :limit => 500, :null => true t.column :remember_token_expires_at, :datetime end - - User.create(:login => 'admin', :name => 'Administrator', :email => '', :password => 'admin', - :password_confirmation => 'admin') end - end diff --git a/sonar-server/src/main/webapp/WEB-INF/db/migrate/066_increase_size_of_rules_profiles_name.rb b/sonar-server/src/main/webapp/WEB-INF/db/migrate/011_create_administrator.rb index 14205a37b86..61d3354ff1c 100644 --- a/sonar-server/src/main/webapp/WEB-INF/db/migrate/066_increase_size_of_rules_profiles_name.rb +++ b/sonar-server/src/main/webapp/WEB-INF/db/migrate/011_create_administrator.rb @@ -17,10 +17,11 @@ # License along with Sonar; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 # -class IncreaseSizeOfRulesProfilesName < ActiveRecord::Migration +class CreateAdministrator < ActiveRecord::Migration def self.up - change_column('rules_profiles', 'name', :string, :limit => 100) + User.create(:login => 'admin', :name => 'Administrator', :email => '', :password => 'admin', + :password_confirmation => 'admin') end -end
\ No newline at end of file +end diff --git a/sonar-server/src/main/webapp/WEB-INF/db/migrate/013_add_metrics_names.rb b/sonar-server/src/main/webapp/WEB-INF/db/migrate/013_add_metrics_names.rb deleted file mode 100644 index 607f8889cb0..00000000000 --- a/sonar-server/src/main/webapp/WEB-INF/db/migrate/013_add_metrics_names.rb +++ /dev/null @@ -1,28 +0,0 @@ -# -# Sonar, entreprise quality control tool. -# Copyright (C) 2008-2011 SonarSource -# mailto:contact AT sonarsource DOT com -# -# Sonar is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 3 of the License, or (at your option) any later version. -# -# Sonar is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with Sonar; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 -# -class AddMetricsNames < ActiveRecord::Migration - - def self.up - add_column(:metrics, :domain, :string, :null => true, :limit => 64) - add_column(:metrics, :short_name, :string, :null => true, :limit => 64) - add_column(:metrics, :qualitative, :boolean, :null => false, :default => false) - end - -end diff --git a/sonar-server/src/main/webapp/WEB-INF/db/migrate/014_create_rules_profiles.rb b/sonar-server/src/main/webapp/WEB-INF/db/migrate/014_create_rules_profiles.rb index 174a90cecd8..1e4f93feb4c 100644 --- a/sonar-server/src/main/webapp/WEB-INF/db/migrate/014_create_rules_profiles.rb +++ b/sonar-server/src/main/webapp/WEB-INF/db/migrate/014_create_rules_profiles.rb @@ -21,8 +21,10 @@ class CreateRulesProfiles < ActiveRecord::Migration def self.up create_table 'rules_profiles'do |t| - t.column :name, :string, :limit => 40, :null => false - t.column :active, :boolean, :default => false + t.column :name, :string, :limit => 100, :null => false + t.column :default_profile, :boolean, :default => false + t.column :provided, :boolean, :default => false, :null => false + t.column 'language', :string, :limit => 16, :null => true end create_table 'active_rules' do |t| diff --git a/sonar-server/src/main/webapp/WEB-INF/db/migrate/016_add_rules_profiles_provided_column.rb b/sonar-server/src/main/webapp/WEB-INF/db/migrate/016_add_rules_profiles_provided_column.rb deleted file mode 100644 index b5697a1d7dd..00000000000 --- a/sonar-server/src/main/webapp/WEB-INF/db/migrate/016_add_rules_profiles_provided_column.rb +++ /dev/null @@ -1,27 +0,0 @@ -# -# Sonar, entreprise quality control tool. -# Copyright (C) 2008-2011 SonarSource -# mailto:contact AT sonarsource DOT com -# -# Sonar is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 3 of the License, or (at your option) any later version. -# -# Sonar is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with Sonar; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 -# -class AddRulesProfilesProvidedColumn < ActiveRecord::Migration - - def self.up - add_column(:rules_profiles, :provided, :boolean, :default => false, :null => false) - Profile.reset_column_information - end - -end diff --git a/sonar-server/src/main/webapp/WEB-INF/db/migrate/041_increase_rules_name_size.rb b/sonar-server/src/main/webapp/WEB-INF/db/migrate/035_create_snapshot_sources.rb index e48f6f6e948..fecb7fab87a 100644 --- a/sonar-server/src/main/webapp/WEB-INF/db/migrate/041_increase_rules_name_size.rb +++ b/sonar-server/src/main/webapp/WEB-INF/db/migrate/035_create_snapshot_sources.rb @@ -17,10 +17,13 @@ # License along with Sonar; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 # -class IncreaseRulesNameSize < ActiveRecord::Migration +class CreateSnapshotSources < ActiveRecord::Migration def self.up - change_column('rules', 'name', :string, :limit => 192) + create_table :snapshot_sources do |t| + t.column :snapshot_id, :integer, :null => false + t.column :data, :text + end + add_index :snapshot_sources, :snapshot_id, :name => 'snap_sources_snapshot_id' end - -end +end
\ No newline at end of file diff --git a/sonar-server/src/main/webapp/WEB-INF/db/migrate/035_projects_to_entities.rb b/sonar-server/src/main/webapp/WEB-INF/db/migrate/035_projects_to_entities.rb deleted file mode 100644 index 0f249815147..00000000000 --- a/sonar-server/src/main/webapp/WEB-INF/db/migrate/035_projects_to_entities.rb +++ /dev/null @@ -1,81 +0,0 @@ -# -# Sonar, entreprise quality control tool. -# Copyright (C) 2008-2011 SonarSource -# mailto:contact AT sonarsource DOT com -# -# Sonar is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 3 of the License, or (at your option) any later version. -# -# Sonar is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with Sonar; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 -# - -require 'project' - -class Project - def branch - read_attribute(:branch) - end -end - -class ProjectsToEntities < ActiveRecord::Migration - def self.up - add_column 'projects', 'scope', :string, :limit => 3 - add_column 'projects', 'qualifier', :string, :limit => 3 - add_column 'projects', 'kee', :string, :limit => 230 - add_column 'projects', 'root_id', :integer - - Project.reset_column_information - - upgrade_snapshots - move_file_sources_to_snapshot_sources - - migrate_distribution_data - end - - private - - def self.migrate_distribution_data - add_column :project_measures, :text_value, :string, :limit => 96, :null => true - ProjectMeasure.reset_column_information - end - - def self.upgrade_snapshots - add_column 'snapshots', 'scope', :string, :limit => 3 - add_column 'snapshots', 'qualifier', :string, :limit => 3 - add_column 'snapshots', 'root_snapshot_id', :integer - - remove_column 'snapshots', 'version' - add_column 'snapshots', 'version', :string, :limit => 32 - - Snapshot.reset_column_information - - begin - remove_index :snapshots, :name => 'snapshot_created_at' - rescue - # the index does not exist (from 1.3) - end - add_index :snapshots, :parent_snapshot_id, :name => 'snapshots_parent' - add_index :snapshots, :root_snapshot_id, :name => 'snapshots_root' - end - - def self.move_file_sources_to_snapshot_sources - create_table :snapshot_sources do |t| - t.column :snapshot_id, :integer, :null => false - t.column :data, :text - end - add_index :snapshot_sources, :snapshot_id, :name => 'snap_sources_snapshot_id' - end - - class RuleFailure035 < ActiveRecord::Base - set_table_name "rule_failures" - end -end
\ No newline at end of file diff --git a/sonar-server/src/main/webapp/WEB-INF/db/migrate/036_add_measure_tendency.rb b/sonar-server/src/main/webapp/WEB-INF/db/migrate/036_add_measure_tendency.rb deleted file mode 100644 index 84c5df8cf08..00000000000 --- a/sonar-server/src/main/webapp/WEB-INF/db/migrate/036_add_measure_tendency.rb +++ /dev/null @@ -1,26 +0,0 @@ -# -# Sonar, entreprise quality control tool. -# Copyright (C) 2008-2011 SonarSource -# mailto:contact AT sonarsource DOT com -# -# Sonar is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 3 of the License, or (at your option) any later version. -# -# Sonar is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with Sonar; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 -# -class AddMeasureTendency < ActiveRecord::Migration - def self.up - add_column 'project_measures', 'tendency', :integer, :null => true - ProjectMeasure.reset_column_information - end - -end
\ No newline at end of file diff --git a/sonar-server/src/main/webapp/WEB-INF/db/migrate/039_add_rules_profiles_language.rb b/sonar-server/src/main/webapp/WEB-INF/db/migrate/039_add_rules_profiles_language.rb deleted file mode 100644 index 76f0cf9a21d..00000000000 --- a/sonar-server/src/main/webapp/WEB-INF/db/migrate/039_add_rules_profiles_language.rb +++ /dev/null @@ -1,26 +0,0 @@ -# -# Sonar, entreprise quality control tool. -# Copyright (C) 2008-2011 SonarSource -# mailto:contact AT sonarsource DOT com -# -# Sonar is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 3 of the License, or (at your option) any later version. -# -# Sonar is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with Sonar; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 -# -class AddRulesProfilesLanguage < ActiveRecord::Migration - - def self.up - add_column 'rules_profiles', 'language', :string, :limit => 16, :null => true - Profile.reset_column_information - end -end diff --git a/sonar-server/src/main/webapp/WEB-INF/db/migrate/046_simplify_metrics.rb b/sonar-server/src/main/webapp/WEB-INF/db/migrate/046_simplify_metrics.rb deleted file mode 100644 index 64fd7c37eb3..00000000000 --- a/sonar-server/src/main/webapp/WEB-INF/db/migrate/046_simplify_metrics.rb +++ /dev/null @@ -1,31 +0,0 @@ -# -# Sonar, entreprise quality control tool. -# Copyright (C) 2008-2011 SonarSource -# mailto:contact AT sonarsource DOT com -# -# Sonar is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 3 of the License, or (at your option) any later version. -# -# Sonar is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with Sonar; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 -# -class SimplifyMetrics < ActiveRecord::Migration - - def self.up - add_column(:metrics, :val_type, :string, :null => true, :limit => 8) - add_column(:metrics, :user_managed, :boolean, :null => true, :default => false) - add_column(:metrics, :enabled, :boolean, :null => true, :default => true) - - remove_column(:metrics, :value_type) - - Metric.reset_column_information - end -end diff --git a/sonar-server/src/main/webapp/WEB-INF/db/migrate/049_remove_external_measures_table.rb b/sonar-server/src/main/webapp/WEB-INF/db/migrate/049_remove_external_measures_table.rb deleted file mode 100644 index 88121955ce2..00000000000 --- a/sonar-server/src/main/webapp/WEB-INF/db/migrate/049_remove_external_measures_table.rb +++ /dev/null @@ -1,40 +0,0 @@ -# -# Sonar, entreprise quality control tool. -# Copyright (C) 2008-2011 SonarSource -# mailto:contact AT sonarsource DOT com -# -# Sonar is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 3 of the License, or (at your option) any later version. -# -# Sonar is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with Sonar; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 -# -class RemoveExternalMeasuresTable < ActiveRecord::Migration - - def self.up - add_columns_to_project_measures - end - - def self.down - - end - - - private - - def self.add_columns_to_project_measures - add_column(:project_measures, :measure_date, :datetime, :null => true) - add_column(:project_measures, :project_id, :integer, :null => true) - change_column(:project_measures, :snapshot_id, :integer, :null => true) - ProjectMeasure.reset_column_information - end - -end diff --git a/sonar-server/src/main/webapp/WEB-INF/db/migrate/051_add_metrics_origin_column.rb b/sonar-server/src/main/webapp/WEB-INF/db/migrate/051_add_metrics_origin_column.rb deleted file mode 100644 index 36f1d2c2954..00000000000 --- a/sonar-server/src/main/webapp/WEB-INF/db/migrate/051_add_metrics_origin_column.rb +++ /dev/null @@ -1,27 +0,0 @@ -# -# Sonar, entreprise quality control tool. -# Copyright (C) 2008-2011 SonarSource -# mailto:contact AT sonarsource DOT com -# -# Sonar is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 3 of the License, or (at your option) any later version. -# -# Sonar is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with Sonar; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 -# -class AddMetricsOriginColumn < ActiveRecord::Migration - - def self.up - add_column(:metrics, :origin, :string, :null => true, :limit => 3) - Metric.reset_column_information - end - -end diff --git a/sonar-server/src/main/webapp/WEB-INF/db/migrate/054_create_alerts_table.rb b/sonar-server/src/main/webapp/WEB-INF/db/migrate/054_create_alerts_table.rb index a03d052cc77..3bf06e3bb1f 100644 --- a/sonar-server/src/main/webapp/WEB-INF/db/migrate/054_create_alerts_table.rb +++ b/sonar-server/src/main/webapp/WEB-INF/db/migrate/054_create_alerts_table.rb @@ -27,9 +27,6 @@ class CreateAlertsTable < ActiveRecord::Migration t.column :value_error, :string, :limit => 64, :null => true t.column :value_warning, :string, :limit => 64, :null => true end - - add_column :project_measures, :alert_status, :string, :limit => 5, :null => true - end end diff --git a/sonar-server/src/main/webapp/WEB-INF/db/migrate/055_create_profiles_per_project.rb b/sonar-server/src/main/webapp/WEB-INF/db/migrate/055_create_profiles_per_project.rb deleted file mode 100644 index ce8a605bd48..00000000000 --- a/sonar-server/src/main/webapp/WEB-INF/db/migrate/055_create_profiles_per_project.rb +++ /dev/null @@ -1,29 +0,0 @@ -# -# Sonar, open source software quality management tool. -# Copyright (C) 2008-2011 SonarSource -# mailto:contact AT sonarsource DOT com -# -# Sonar is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 3 of the License, or (at your option) any later version. -# -# Sonar is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with Sonar; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 -# -class CreateProfilesPerProject < ActiveRecord::Migration - - def self.up - add_column :projects, :profile_id, :integer, :null => true - Project.reset_column_information - rename_column :rules_profiles, :active, :default_profile - Profile.reset_column_information - end - -end
\ No newline at end of file diff --git a/sonar-server/src/main/webapp/WEB-INF/db/migrate/057_ensure_measure_snapshot_column_null.rb b/sonar-server/src/main/webapp/WEB-INF/db/migrate/057_ensure_measure_snapshot_column_null.rb deleted file mode 100644 index b63aef33688..00000000000 --- a/sonar-server/src/main/webapp/WEB-INF/db/migrate/057_ensure_measure_snapshot_column_null.rb +++ /dev/null @@ -1,37 +0,0 @@ -# -# Sonar, open source software quality management tool. -# Copyright (C) 2008-2011 SonarSource -# mailto:contact AT sonarsource DOT com -# -# Sonar is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 3 of the License, or (at your option) any later version. -# -# Sonar is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with Sonar; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 -# -class EnsureMeasureSnapshotColumnNull < ActiveRecord::Migration - - def self.up - # due to a migration issue under oracle with previous version, we have to make sure that this colum - # is set to null even if already done in migration 49 - begin - change_column(:project_measures, :snapshot_id, :integer, :null => true) - rescue - puts "project_measures.snapshot_id already set to nullable" - end - - end - - def self.down - - end - -end
\ No newline at end of file diff --git a/sonar-server/src/main/webapp/WEB-INF/db/migrate/058_add_snapshots_path.rb b/sonar-server/src/main/webapp/WEB-INF/db/migrate/058_add_snapshots_path.rb deleted file mode 100644 index 4307aa28eb3..00000000000 --- a/sonar-server/src/main/webapp/WEB-INF/db/migrate/058_add_snapshots_path.rb +++ /dev/null @@ -1,32 +0,0 @@ -# -# Sonar, open source software quality management tool. -# Copyright (C) 2008-2011 SonarSource -# mailto:contact AT sonarsource DOT com -# -# Sonar is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 3 of the License, or (at your option) any later version. -# -# Sonar is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with Sonar; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 -# -class AddSnapshotsPath < ActiveRecord::Migration - - def self.up - add_column(:snapshots, :path, :string, :null => true, :limit => 96) - add_column(:snapshots, :depth, :integer, :null => true) - Snapshot.reset_column_information - end - - def self.down - - end - -end
\ No newline at end of file diff --git a/sonar-server/src/main/webapp/WEB-INF/db/migrate/059_add_properties_resource.rb b/sonar-server/src/main/webapp/WEB-INF/db/migrate/059_create_properties.rb index c724469946c..d0b33566669 100644 --- a/sonar-server/src/main/webapp/WEB-INF/db/migrate/059_add_properties_resource.rb +++ b/sonar-server/src/main/webapp/WEB-INF/db/migrate/059_create_properties.rb @@ -17,13 +17,13 @@ # License along with Sonar; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 # -class AddPropertiesResource < ActiveRecord::Migration +class CreateProperties < ActiveRecord::Migration def self.up create_table 'properties' do |t| t.column :prop_key, :string, :limit => 512 t.column :resource_id, :integer, :null => true - t.column :prop_value, :string, :limit => 4000 + t.column :text_value, :text, :null => true end add_index :properties, :prop_key, :name => 'properties_key' end diff --git a/sonar-server/src/main/webapp/WEB-INF/db/migrate/060_add_project_language.rb b/sonar-server/src/main/webapp/WEB-INF/db/migrate/060_add_project_language.rb deleted file mode 100644 index 2b110b22761..00000000000 --- a/sonar-server/src/main/webapp/WEB-INF/db/migrate/060_add_project_language.rb +++ /dev/null @@ -1,27 +0,0 @@ -# -# Sonar, open source software quality management tool. -# Copyright (C) 2008-2011 SonarSource -# mailto:contact AT sonarsource DOT com -# -# Sonar is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 3 of the License, or (at your option) any later version. -# -# Sonar is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with Sonar; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 -# -class AddProjectLanguage < ActiveRecord::Migration - - def self.up - add_column(:projects, :language, :string, :null => true, :limit => 5) - Project.reset_column_information - end - -end diff --git a/sonar-server/src/main/webapp/WEB-INF/db/migrate/061_add_measure_data.rb b/sonar-server/src/main/webapp/WEB-INF/db/migrate/061_add_measure_data.rb index a2b6a43c807..0b3d43418fb 100644 --- a/sonar-server/src/main/webapp/WEB-INF/db/migrate/061_add_measure_data.rb +++ b/sonar-server/src/main/webapp/WEB-INF/db/migrate/061_add_measure_data.rb @@ -26,20 +26,11 @@ class AddMeasureData < ActiveRecord::Migration t.column :data, :binary, :null => true end add_index :measure_data, :measure_id, :name => 'measure_data_measure_id' - MeasureData061.reset_column_information - - add_column(:project_measures, :alert_text, :string, :null => true, :limit => 4000) - add_column(:project_measures, :url, :string, :null => true, :limit => 2000) - add_column(:project_measures, :description, :string, :null => true, :limit => 4000) - ProjectMeasure.reset_column_information + MeasureData061.reset_column_information end class MeasureData061 < ActiveRecord::Base set_table_name :measure_data end - class ProjectMeasure61 < ActiveRecord::Base - set_table_name :project_measures - end - end
\ No newline at end of file diff --git a/sonar-server/src/main/webapp/WEB-INF/db/migrate/068_add_rule_priority.rb b/sonar-server/src/main/webapp/WEB-INF/db/migrate/068_add_rule_priority.rb deleted file mode 100644 index e53aadd92a0..00000000000 --- a/sonar-server/src/main/webapp/WEB-INF/db/migrate/068_add_rule_priority.rb +++ /dev/null @@ -1,50 +0,0 @@ -# -# Sonar, entreprise quality control tool. -# Copyright (C) 2008-2011 SonarSource -# mailto:contact AT sonarsource DOT com -# -# Sonar is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 3 of the License, or (at your option) any later version. -# -# Sonar is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with Sonar; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 -# -class AddRulePriority < ActiveRecord::Migration - - def self.up - add_column(:rules, :priority, :integer, :null => true) - Rule.reset_column_information - - add_column(:project_measures, :rule_priority, :integer, :null => true) - ProjectMeasure.reset_column_information - - violations = Metric.find(:first, :conditions => {:name => Metric::VIOLATIONS}) - - mandatory_violations_density = Metric.find(:first, :conditions => {:name => 'mandatory_violations_density'}) - ProjectMeasure.delete_all("metric_id=" + mandatory_violations_density.id.to_s) if mandatory_violations_density - - mandatory_violations = Metric.find(:first, :conditions => {:name => 'mandatory_violations'}) - if mandatory_violations and violations - ProjectMeasure.update_all("rule_priority=" + Sonar::RulePriority::PRIORITY_MAJOR.to_s, "metric_id=" + mandatory_violations.id.to_s) - ProjectMeasure.update_all("metric_id=" + violations.id.to_s, "metric_id=" + mandatory_violations.id.to_s) - end - - optional_violations = Metric.find(:first, :conditions => {:name => 'optional_violations'}) - if optional_violations and violations - ProjectMeasure.update_all("rule_priority=" + Sonar::RulePriority::PRIORITY_INFO.to_s, "metric_id=" + optional_violations.id.to_s) - ProjectMeasure.update_all("metric_id=" + violations.id.to_s, "metric_id=" + optional_violations.id.to_s) - end - - #SONAR-1062 Active rules with optional level are not converted to priority INFO during 1.10 migration - ActiveRule.update_all('failure_level=0', 'failure_level=1') - end - -end
\ No newline at end of file diff --git a/sonar-server/src/main/webapp/WEB-INF/db/migrate/069_add_diff_columns_to_measures.rb b/sonar-server/src/main/webapp/WEB-INF/db/migrate/069_add_diff_columns_to_measures.rb deleted file mode 100644 index cfacedc0dc2..00000000000 --- a/sonar-server/src/main/webapp/WEB-INF/db/migrate/069_add_diff_columns_to_measures.rb +++ /dev/null @@ -1,28 +0,0 @@ -# -# Sonar, entreprise quality control tool. -# Copyright (C) 2008-2011 SonarSource -# mailto:contact AT sonarsource DOT com -# -# Sonar is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 3 of the License, or (at your option) any later version. -# -# Sonar is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with Sonar; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 -# -class AddDiffColumnsToMeasures < ActiveRecord::Migration - - def self.up - add_column(:project_measures, :diff_value_1, :decimal, :null => true, :precision => 30, :scale => 20) - add_column(:project_measures, :diff_value_2, :decimal, :null => true, :precision => 30, :scale => 20) - add_column(:project_measures, :diff_value_3, :decimal, :null => true, :precision => 30, :scale => 20) - end - -end
\ No newline at end of file diff --git a/sonar-server/src/main/webapp/WEB-INF/db/migrate/072_delete_snapshots_purged.rb b/sonar-server/src/main/webapp/WEB-INF/db/migrate/072_delete_snapshots_purged.rb deleted file mode 100644 index 91111464251..00000000000 --- a/sonar-server/src/main/webapp/WEB-INF/db/migrate/072_delete_snapshots_purged.rb +++ /dev/null @@ -1,26 +0,0 @@ - # - # Sonar, entreprise quality control tool. - # Copyright (C) 2008-2011 SonarSource - # mailto:contact AT sonarsource DOT com - # - # Sonar is free software; you can redistribute it and/or - # modify it under the terms of the GNU Lesser General Public - # License as published by the Free Software Foundation; either - # version 3 of the License, or (at your option) any later version. - # - # Sonar is distributed in the hope that it will be useful, - # but WITHOUT ANY WARRANTY; without even the implied warranty of - # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - # Lesser General Public License for more details. - # - # You should have received a copy of the GNU Lesser General Public - # License along with Sonar; if not, write to the Free Software - # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 - # -class DeleteSnapshotsPurged < ActiveRecord::Migration - - def self.up - remove_column(:snapshots, :purged) - Snapshot.reset_column_information - end -end
\ No newline at end of file diff --git a/sonar-server/src/main/webapp/WEB-INF/db/migrate/073_add_line_to_rule_failures.rb b/sonar-server/src/main/webapp/WEB-INF/db/migrate/073_add_line_to_rule_failures.rb deleted file mode 100644 index 70b189784ca..00000000000 --- a/sonar-server/src/main/webapp/WEB-INF/db/migrate/073_add_line_to_rule_failures.rb +++ /dev/null @@ -1,27 +0,0 @@ - # - # Sonar, entreprise quality control tool. - # Copyright (C) 2008-2011 SonarSource - # mailto:contact AT sonarsource DOT com - # - # Sonar is free software; you can redistribute it and/or - # modify it under the terms of the GNU Lesser General Public - # License as published by the Free Software Foundation; either - # version 3 of the License, or (at your option) any later version. - # - # Sonar is distributed in the hope that it will be useful, - # but WITHOUT ANY WARRANTY; without even the implied warranty of - # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - # Lesser General Public License for more details. - # - # You should have received a copy of the GNU Lesser General Public - # License along with Sonar; if not, write to the Free Software - # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 - # -class AddLineToRuleFailures < ActiveRecord::Migration - - def self.up - add_column :rule_failures, :line, :integer, :null => true - RuleFailure.reset_column_information - end - -end
\ No newline at end of file diff --git a/sonar-server/src/main/webapp/WEB-INF/db/migrate/075_reset_tendency_depth.rb b/sonar-server/src/main/webapp/WEB-INF/db/migrate/075_reset_tendency_depth.rb deleted file mode 100644 index ba00735f89e..00000000000 --- a/sonar-server/src/main/webapp/WEB-INF/db/migrate/075_reset_tendency_depth.rb +++ /dev/null @@ -1,29 +0,0 @@ - # - # Sonar, entreprise quality control tool. - # Copyright (C) 2008-2011 SonarSource - # mailto:contact AT sonarsource DOT com - # - # Sonar is free software; you can redistribute it and/or - # modify it under the terms of the GNU Lesser General Public - # License as published by the Free Software Foundation; either - # version 3 of the License, or (at your option) any later version. - # - # Sonar is distributed in the hope that it will be useful, - # but WITHOUT ANY WARRANTY; without even the implied warranty of - # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - # Lesser General Public License for more details. - # - # You should have received a copy of the GNU Lesser General Public - # License along with Sonar; if not, write to the Free Software - # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 - # -class ResetTendencyDepth < ActiveRecord::Migration - - def self.up - begin - Property.delete_all("prop_key='tendency.depth'") - rescue - end - end - -end
\ No newline at end of file diff --git a/sonar-server/src/main/webapp/WEB-INF/db/migrate/076_upgrade_properties_to_blobs.rb b/sonar-server/src/main/webapp/WEB-INF/db/migrate/076_upgrade_properties_to_blobs.rb deleted file mode 100644 index cc361ccfcd4..00000000000 --- a/sonar-server/src/main/webapp/WEB-INF/db/migrate/076_upgrade_properties_to_blobs.rb +++ /dev/null @@ -1,35 +0,0 @@ - # - # Sonar, entreprise quality control tool. - # Copyright (C) 2008-2011 SonarSource - # mailto:contact AT sonarsource DOT com - # - # Sonar is free software; you can redistribute it and/or - # modify it under the terms of the GNU Lesser General Public - # License as published by the Free Software Foundation; either - # version 3 of the License, or (at your option) any later version. - # - # Sonar is distributed in the hope that it will be useful, - # but WITHOUT ANY WARRANTY; without even the implied warranty of - # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - # Lesser General Public License for more details. - # - # You should have received a copy of the GNU Lesser General Public - # License along with Sonar; if not, write to the Free Software - # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 - # -class UpgradePropertiesToBlobs < ActiveRecord::Migration - - def self.up - add_column(:properties, :text_value, :text, :null => true) - Property.reset_column_information - - Property.find(:all).each do |p| - p.text_value=p.prop_value - p.save! - end - - remove_column(:properties, :prop_value) - Property.reset_column_information - end - -end
\ No newline at end of file diff --git a/sonar-server/src/main/webapp/WEB-INF/db/migrate/077_add_copy_resource_id_to_projects.rb b/sonar-server/src/main/webapp/WEB-INF/db/migrate/077_add_copy_resource_id_to_projects.rb deleted file mode 100644 index 303920a64ae..00000000000 --- a/sonar-server/src/main/webapp/WEB-INF/db/migrate/077_add_copy_resource_id_to_projects.rb +++ /dev/null @@ -1,26 +0,0 @@ - # - # Sonar, entreprise quality control tool. - # Copyright (C) 2008-2011 SonarSource - # mailto:contact AT sonarsource DOT com - # - # Sonar is free software; you can redistribute it and/or - # modify it under the terms of the GNU Lesser General Public - # License as published by the Free Software Foundation; either - # version 3 of the License, or (at your option) any later version. - # - # Sonar is distributed in the hope that it will be useful, - # but WITHOUT ANY WARRANTY; without even the implied warranty of - # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - # Lesser General Public License for more details. - # - # You should have received a copy of the GNU Lesser General Public - # License along with Sonar; if not, write to the Free Software - # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 - # -class AddCopyResourceIdToProjects < ActiveRecord::Migration - - def self.up - add_column(:projects, :copy_resource_id, :integer, :null => true) - end - -end
\ No newline at end of file diff --git a/sonar-server/src/main/webapp/WEB-INF/db/migrate/078_increase_project_kee_size.rb b/sonar-server/src/main/webapp/WEB-INF/db/migrate/078_increase_project_kee_size.rb deleted file mode 100644 index 074dd969836..00000000000 --- a/sonar-server/src/main/webapp/WEB-INF/db/migrate/078_increase_project_kee_size.rb +++ /dev/null @@ -1,26 +0,0 @@ - # - # Sonar, entreprise quality control tool. - # Copyright (C) 2008-2011 SonarSource - # mailto:contact AT sonarsource DOT com - # - # Sonar is free software; you can redistribute it and/or - # modify it under the terms of the GNU Lesser General Public - # License as published by the Free Software Foundation; either - # version 3 of the License, or (at your option) any later version. - # - # Sonar is distributed in the hope that it will be useful, - # but WITHOUT ANY WARRANTY; without even the implied warranty of - # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - # Lesser General Public License for more details. - # - # You should have received a copy of the GNU Lesser General Public - # License along with Sonar; if not, write to the Free Software - # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 - # -class IncreaseProjectKeeSize < ActiveRecord::Migration - - def self.up - change_column('projects', 'kee', :string, :limit => 400) - end - -end
\ No newline at end of file diff --git a/sonar-server/src/main/webapp/WEB-INF/db/migrate/081_add_projects_long_name.rb b/sonar-server/src/main/webapp/WEB-INF/db/migrate/081_add_projects_long_name.rb deleted file mode 100644 index bb8758ceeab..00000000000 --- a/sonar-server/src/main/webapp/WEB-INF/db/migrate/081_add_projects_long_name.rb +++ /dev/null @@ -1,27 +0,0 @@ - # - # Sonar, entreprise quality control tool. - # Copyright (C) 2008-2011 SonarSource - # mailto:contact AT sonarsource DOT com - # - # Sonar is free software; you can redistribute it and/or - # modify it under the terms of the GNU Lesser General Public - # License as published by the Free Software Foundation; either - # version 3 of the License, or (at your option) any later version. - # - # Sonar is distributed in the hope that it will be useful, - # but WITHOUT ANY WARRANTY; without even the implied warranty of - # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - # Lesser General Public License for more details. - # - # You should have received a copy of the GNU Lesser General Public - # License along with Sonar; if not, write to the Free Software - # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 - # -class AddProjectsLongName < ActiveRecord::Migration - - def self.up - add_column :projects, :long_name, :string, :null => true, :limit => 256 - Project.reset_column_information - end - -end
\ No newline at end of file diff --git a/sonar-server/src/main/webapp/WEB-INF/db/migrate/082_include_branch_in_project_name.rb b/sonar-server/src/main/webapp/WEB-INF/db/migrate/082_include_branch_in_project_name.rb deleted file mode 100644 index 6b36ffd5ecc..00000000000 --- a/sonar-server/src/main/webapp/WEB-INF/db/migrate/082_include_branch_in_project_name.rb +++ /dev/null @@ -1,40 +0,0 @@ - # - # Sonar, entreprise quality control tool. - # Copyright (C) 2008-2011 SonarSource - # mailto:contact AT sonarsource DOT com - # - # Sonar is free software; you can redistribute it and/or - # modify it under the terms of the GNU Lesser General Public - # License as published by the Free Software Foundation; either - # version 3 of the License, or (at your option) any later version. - # - # Sonar is distributed in the hope that it will be useful, - # but WITHOUT ANY WARRANTY; without even the implied warranty of - # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - # Lesser General Public License for more details. - # - # You should have received a copy of the GNU Lesser General Public - # License along with Sonar; if not, write to the Free Software - # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 - # -class IncludeBranchInProjectName < ActiveRecord::Migration - - def self.up - Project.find(:all, :conditions => {:scope => Project::SCOPE_SET, :qualifier => Project::QUALIFIER_PROJECT}).each do |project| - branch=branch(project) - if branch - project.name+= ' ' + branch - project.save! - end - end - end - - private - def self.branch(project) - s=project.kee.split(':') - if s.size>=3 - return s[2] - end - nil - end -end
\ No newline at end of file diff --git a/sonar-server/src/main/webapp/WEB-INF/db/migrate/084_delete_some_findbugs_rules.rb b/sonar-server/src/main/webapp/WEB-INF/db/migrate/084_delete_some_findbugs_rules.rb deleted file mode 100644 index a4a97eba046..00000000000 --- a/sonar-server/src/main/webapp/WEB-INF/db/migrate/084_delete_some_findbugs_rules.rb +++ /dev/null @@ -1,38 +0,0 @@ - # - # Sonar, entreprise quality control tool. - # Copyright (C) 2008-2011 SonarSource - # mailto:contact AT sonarsource DOT com - # - # Sonar is free software; you can redistribute it and/or - # modify it under the terms of the GNU Lesser General Public - # License as published by the Free Software Foundation; either - # version 3 of the License, or (at your option) any later version. - # - # Sonar is distributed in the hope that it will be useful, - # but WITHOUT ANY WARRANTY; without even the implied warranty of - # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - # Lesser General Public License for more details. - # - # You should have received a copy of the GNU Lesser General Public - # License along with Sonar; if not, write to the Free Software - # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 - # -class DeleteSomeFindbugsRules < ActiveRecord::Migration - - def self.up - delete_rule('EQ_DOESNT_OVERRIDE_EQUALS') - delete_rule('OBL_UNSATISFIED_OBLIGATION') - end - - private - def self.delete_rule(rule_key) - rule=Rule.find(:first, :conditions => {:plugin_name => 'findbugs', :plugin_rule_key => rule_key}) - if rule - say_with_time "Deleting Findbugs rule #{rule_key}..." do - rule_id=rule.id - ActiveRule.delete_all(["rule_id=?", rule_id]) - rule.delete - end - end - end -end
\ No newline at end of file diff --git a/sonar-server/src/main/webapp/WEB-INF/db/migrate/085_add_metric_value_limits.rb b/sonar-server/src/main/webapp/WEB-INF/db/migrate/085_add_metric_value_limits.rb deleted file mode 100644 index 62760eb619e..00000000000 --- a/sonar-server/src/main/webapp/WEB-INF/db/migrate/085_add_metric_value_limits.rb +++ /dev/null @@ -1,35 +0,0 @@ - # - # Sonar, entreprise quality control tool. - # Copyright (C) 2008-2011 SonarSource - # mailto:contact AT sonarsource DOT com - # - # Sonar is free software; you can redistribute it and/or - # modify it under the terms of the GNU Lesser General Public - # License as published by the Free Software Foundation; either - # version 3 of the License, or (at your option) any later version. - # - # Sonar is distributed in the hope that it will be useful, - # but WITHOUT ANY WARRANTY; without even the implied warranty of - # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - # Lesser General Public License for more details. - # - # You should have received a copy of the GNU Lesser General Public - # License along with Sonar; if not, write to the Free Software - # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 - # - -# see SONAR-1075 -class AddMetricValueLimits < ActiveRecord::Migration - - def self.up - add_column 'metrics', 'worst_value', :decimal, :null => true, :precision => 30, :scale => 20 - add_column 'metrics', 'best_value', :decimal, :null => true, :precision => 30, :scale => 20 - add_column 'metrics', 'optimized_best_value', :boolean, :null => true - end - - def self.down - remove_column 'metrics', 'worst_value' - remove_column 'metrics', 'best_value' - remove_column 'metrics', 'optimized_best_value' - end -end
\ No newline at end of file diff --git a/sonar-server/src/main/webapp/WEB-INF/db/migrate/090_add_name_to_users.rb b/sonar-server/src/main/webapp/WEB-INF/db/migrate/090_add_name_to_users.rb index e8076d80ab3..915b172192c 100644 --- a/sonar-server/src/main/webapp/WEB-INF/db/migrate/090_add_name_to_users.rb +++ b/sonar-server/src/main/webapp/WEB-INF/db/migrate/090_add_name_to_users.rb @@ -32,8 +32,4 @@ class AddNameToUsers < ActiveRecord::Migration end - def self.down - - end - end diff --git a/sonar-server/src/main/webapp/WEB-INF/db/migrate/091_add_root_project_id_to_snapshots.rb b/sonar-server/src/main/webapp/WEB-INF/db/migrate/091_add_root_project_id_to_snapshots.rb deleted file mode 100644 index a667100b95e..00000000000 --- a/sonar-server/src/main/webapp/WEB-INF/db/migrate/091_add_root_project_id_to_snapshots.rb +++ /dev/null @@ -1,31 +0,0 @@ -# -# Sonar, entreprise quality control tool. -# Copyright (C) 2008-2011 SonarSource -# mailto:contact AT sonarsource DOT com -# -# Sonar is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 3 of the License, or (at your option) any later version. -# -# Sonar is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with Sonar; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 -# -class AddRootProjectIdToSnapshots < ActiveRecord::Migration - - def self.up - add_column(:snapshots, :root_project_id, :integer, :nullable => true) - Snapshot.reset_column_information - end - - def self.down - - end - -end diff --git a/sonar-server/src/main/webapp/WEB-INF/db/migrate/093_create_dependencies_table.rb b/sonar-server/src/main/webapp/WEB-INF/db/migrate/093_create_dependencies_table.rb index 7b90fb7f035..650d69ee8c8 100644 --- a/sonar-server/src/main/webapp/WEB-INF/db/migrate/093_create_dependencies_table.rb +++ b/sonar-server/src/main/webapp/WEB-INF/db/migrate/093_create_dependencies_table.rb @@ -25,7 +25,7 @@ class CreateDependenciesTable < ActiveRecord::Migration t.column :from_resource_id, :integer, :null => true t.column :to_snapshot_id, :integer, :null => true t.column :to_resource_id, :integer, :null => true - t.column :dep_usage, :string, :null => true, :limit => 15 + t.column :dep_usage, :string, :null => true, :limit => 30 t.column :dep_weight, :integer, :null => true t.column :project_snapshot_id, :integer, :null => true t.column :parent_dependency_id, :big_integer, :null => true @@ -38,10 +38,4 @@ class CreateDependenciesTable < ActiveRecord::Migration add_index :dependencies, :to_snapshot_id, :name => 'deps_to_sid' end - def self.down - remove_index :dependencies, :name => 'deps_from_sid' - remove_index :dependencies, :name => 'deps_to_sid' - drop_table :dependencies - end - end diff --git a/sonar-server/src/main/webapp/WEB-INF/db/migrate/094_add_metrics_hidden_column.rb b/sonar-server/src/main/webapp/WEB-INF/db/migrate/094_add_metrics_hidden_column.rb deleted file mode 100644 index c5526de568e..00000000000 --- a/sonar-server/src/main/webapp/WEB-INF/db/migrate/094_add_metrics_hidden_column.rb +++ /dev/null @@ -1,31 +0,0 @@ -# -# Sonar, entreprise quality control tool. -# Copyright (C) 2008-2011 SonarSource -# mailto:contact AT sonarsource DOT com -# -# Sonar is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 3 of the License, or (at your option) any later version. -# -# Sonar is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with Sonar; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 -# -class AddMetricsHiddenColumn < ActiveRecord::Migration - - def self.up - add_column 'metrics', 'hidden', :boolean, :null => true - Metric.reset_column_information - Metric.clear_cache - end - - def self.down - remove_column 'metrics', 'hidden' - end -end diff --git a/sonar-server/src/main/webapp/WEB-INF/db/migrate/095_increase_measure_id_size.rb b/sonar-server/src/main/webapp/WEB-INF/db/migrate/095_increase_measure_id_size.rb index 14991a2a6a6..6f4139a9c1e 100644 --- a/sonar-server/src/main/webapp/WEB-INF/db/migrate/095_increase_measure_id_size.rb +++ b/sonar-server/src/main/webapp/WEB-INF/db/migrate/095_increase_measure_id_size.rb @@ -25,7 +25,4 @@ class IncreaseMeasureIdSize < ActiveRecord::Migration alter_to_big_integer('async_measure_snapshots', 'project_measure_id', 'async_m_s_measure_id') end - def self.down - - end end diff --git a/sonar-server/src/main/webapp/WEB-INF/db/migrate/097_increase_dep_usage_size.rb b/sonar-server/src/main/webapp/WEB-INF/db/migrate/097_increase_dep_usage_size.rb deleted file mode 100644 index 7322b8c8376..00000000000 --- a/sonar-server/src/main/webapp/WEB-INF/db/migrate/097_increase_dep_usage_size.rb +++ /dev/null @@ -1,29 +0,0 @@ -# -# Sonar, entreprise quality control tool. -# Copyright (C) 2008-2011 SonarSource -# mailto:contact AT sonarsource DOT com -# -# Sonar is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 3 of the License, or (at your option) any later version. -# -# Sonar is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with Sonar; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 -# -class IncreaseDepUsageSize < ActiveRecord::Migration - - def self.up - change_column('dependencies', 'dep_usage', :string, :limit => 30, :null => true) - end - - def self.down - - end -end diff --git a/sonar-server/src/main/webapp/WEB-INF/db/migrate/098_increase_snapshots_version.rb b/sonar-server/src/main/webapp/WEB-INF/db/migrate/098_increase_snapshots_version.rb deleted file mode 100644 index ede5e05dc5d..00000000000 --- a/sonar-server/src/main/webapp/WEB-INF/db/migrate/098_increase_snapshots_version.rb +++ /dev/null @@ -1,29 +0,0 @@ -# -# Sonar, entreprise quality control tool. -# Copyright (C) 2008-2011 SonarSource -# mailto:contact AT sonarsource DOT com -# -# Sonar is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 3 of the License, or (at your option) any later version. -# -# Sonar is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with Sonar; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 -# -class IncreaseSnapshotsVersion < ActiveRecord::Migration - - def self.up - change_column('snapshots', 'version', :string, :limit => 60, :null => true) - end - - def self.down - - end -end diff --git a/sonar-server/src/main/webapp/WEB-INF/db/migrate/099_delete_deprecated_libraries.rb b/sonar-server/src/main/webapp/WEB-INF/db/migrate/099_delete_deprecated_libraries.rb deleted file mode 100644 index e9ba4bffa59..00000000000 --- a/sonar-server/src/main/webapp/WEB-INF/db/migrate/099_delete_deprecated_libraries.rb +++ /dev/null @@ -1,39 +0,0 @@ -# -# Sonar, entreprise quality control tool. -# Copyright (C) 2008-2011 SonarSource -# mailto:contact AT sonarsource DOT com -# -# Sonar is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 3 of the License, or (at your option) any later version. -# -# Sonar is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with Sonar; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 -# -class DeleteDeprecatedLibraries < ActiveRecord::Migration - - def self.up - say_with_time "Deleting deprecated dependencies on libs..." do - Dependency.delete_all("from_scope='LIB' OR to_scope='LIB'") - end - - say_with_time "Deleting deprecated snapshots on libs..." do - Snapshot.delete_all("scope='LIB'") - end - - say_with_time "Deleting deprecated libs..." do - Project.delete_all("scope='LIB'") - end - end - - def self.down - - end -end diff --git a/sonar-server/src/main/webapp/WEB-INF/db/migrate/100_delete_checkstyle_regexp_rules.rb b/sonar-server/src/main/webapp/WEB-INF/db/migrate/100_delete_checkstyle_regexp_rules.rb deleted file mode 100644 index 7af3cdb223f..00000000000 --- a/sonar-server/src/main/webapp/WEB-INF/db/migrate/100_delete_checkstyle_regexp_rules.rb +++ /dev/null @@ -1,43 +0,0 @@ - # - # Sonar, entreprise quality control tool. - # Copyright (C) 2008-2011 SonarSource - # mailto:contact AT sonarsource DOT com - # - # Sonar is free software; you can redistribute it and/or - # modify it under the terms of the GNU Lesser General Public - # License as published by the Free Software Foundation; either - # version 3 of the License, or (at your option) any later version. - # - # Sonar is distributed in the hope that it will be useful, - # but WITHOUT ANY WARRANTY; without even the implied warranty of - # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - # Lesser General Public License for more details. - # - # You should have received a copy of the GNU Lesser General Public - # License along with Sonar; if not, write to the Free Software - # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 - # -class DeleteCheckstyleRegexpRules < ActiveRecord::Migration - - def self.up - delete_rule('com.puppycrawl.tools.checkstyle.checks.RegexpMultilineCheck') - delete_rule('com.puppycrawl.tools.checkstyle.checks.RegexpSinglelineCheck') - delete_rule('com.puppycrawl.tools.checkstyle.checks.RegexpSinglelineJavaCheck') - end - - def self.down - - end - - private - def self.delete_rule(rule_key) - rule=Rule.find(:first, :conditions => {:plugin_name => 'checkstyle', :plugin_rule_key => rule_key}) - if rule - say_with_time "Deleting Checkstyle rule #{rule_key}..." do - rule_id=rule.id - ActiveRule.destroy_all(["rule_id=?", rule_id]) - rule.destroy - end - end - end -end
\ No newline at end of file diff --git a/sonar-server/src/main/webapp/WEB-INF/db/migrate/092_fill_snapshots_root_project_id.rb b/sonar-server/src/main/webapp/WEB-INF/db/migrate/217_create_duplications_index.rb index 351cf6f0789..18ebb0806e6 100644 --- a/sonar-server/src/main/webapp/WEB-INF/db/migrate/092_fill_snapshots_root_project_id.rb +++ b/sonar-server/src/main/webapp/WEB-INF/db/migrate/217_create_duplications_index.rb @@ -17,26 +17,25 @@ # License along with Sonar; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 # -class FillSnapshotsRootProjectId < ActiveRecord::Migration + +# +# Sonar 2.11 +# +class CreateDuplicationsIndex < ActiveRecord::Migration def self.up - snapshots=Snapshot.find(:all, :select => 'id,root_snapshot_id', :conditions => ['islast=?', true]) - root_sids=[] - snapshots.each do |s| - root_sids<< (s.root_snapshot_id || s.id) + create_table :duplications_index do |t| + t.column :project_snapshot_id, :integer, :null => false + t.column :snapshot_id, :integer, :null => false + t.column :hash, :string, :null => false, :limit => 50 + t.column :index_in_file, :integer, :null => false + t.column :start_line, :integer, :null => false + t.column :end_line, :integer, :null => false end - root_sids=root_sids.uniq.compact - - root_sids.each do |root_sid| - root_snapshot=Snapshot.find(:first, :conditions => {:id => root_sid}) - if root_snapshot - Snapshot.update_all("root_project_id=#{root_snapshot.project_id}", ['root_snapshot_id=? or id=?', root_sid, root_sid]) - end - end - end - - def self.down + add_index :duplications_index, :project_snapshot_id, :name => 'duplications_index_psid' + add_index :duplications_index, :snapshot_id, :name => 'duplications_index_sid' + add_index :duplications_index, :hash, :name => 'duplications_index_hash' end end diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-0.9.0.1/.specification b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-0.9.0.1/.specification deleted file mode 100755 index 461e919c99f..00000000000 --- a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-0.9.0.1/.specification +++ /dev/null @@ -1,162 +0,0 @@ ---- !ruby/object:Gem::Specification -name: activerecord-jdbc-adapter -version: !ruby/object:Gem::Version - prerelease: false - segments: - - 0 - - 9 - - 0 - - 1 - version: 0.9.0.1 -platform: ruby -authors: - - Nick Sieger, Ola Bini and JRuby contributors -autorequire: -bindir: bin -cert_chain: [] - -date: 2009-10-26 00:00:00 +01:00 -default_executable: -dependencies: [] - -description: |- - activerecord-jdbc-adapter is a database adapter for Rails' ActiveRecord - component that can be used with JRuby[http://www.jruby.org/]. It allows use of - virtually any JDBC-compliant database with your JRuby on Rails application. -email: nick@nicksieger.com, ola.bini@gmail.com -executables: [] - -extensions: [] - -extra_rdoc_files: - - History.txt - - Manifest.txt - - README.txt - - LICENSE.txt -files: - - History.txt - - Manifest.txt - - README.txt - - Rakefile - - LICENSE.txt - - lib/active_record/connection_adapters/cachedb_adapter.rb - - lib/active_record/connection_adapters/derby_adapter.rb - - lib/active_record/connection_adapters/h2_adapter.rb - - lib/active_record/connection_adapters/hsqldb_adapter.rb - - lib/active_record/connection_adapters/informix_adapter.rb - - lib/active_record/connection_adapters/jdbc_adapter.rb - - lib/active_record/connection_adapters/jdbc_adapter_spec.rb - - lib/active_record/connection_adapters/jndi_adapter.rb - - lib/active_record/connection_adapters/mysql_adapter.rb - - lib/active_record/connection_adapters/oracle_adapter.rb - - lib/active_record/connection_adapters/postgresql_adapter.rb - - lib/active_record/connection_adapters/sqlite3_adapter.rb - - lib/jdbc_adapter/jdbc_cachedb.rb - - lib/jdbc_adapter/jdbc_db2.rb - - lib/jdbc_adapter/jdbc_derby.rb - - lib/jdbc_adapter/jdbc_firebird.rb - - lib/jdbc_adapter/jdbc_hsqldb.rb - - lib/jdbc_adapter/jdbc_informix.rb - - lib/jdbc_adapter/jdbc_mimer.rb - - lib/jdbc_adapter/jdbc_mssql.rb - - lib/jdbc_adapter/jdbc_mysql.rb - - lib/jdbc_adapter/jdbc_oracle.rb - - lib/jdbc_adapter/jdbc_postgre.rb - - lib/jdbc_adapter/jdbc_sqlite3.rb - - lib/jdbc_adapter/jdbc_sybase.rb - - lib/jdbc_adapter/missing_functionality_helper.rb - - lib/jdbc_adapter/rake_tasks.rb - - lib/jdbc_adapter/tsql_helper.rb - - lib/jdbc_adapter/version.rb - - lib/jdbc_adapter.rb - - lib/jdbc_adapter/jdbc_adapter_internal.jar - - test/activerecord/connection_adapters/type_conversion_test.rb - - test/activerecord/connections/native_jdbc_mysql/connection.rb - - test/cachedb_simple_test.rb - - test/db/cachedb.rb - - test/db/db2.rb - - test/db/derby.rb - - test/db/h2.rb - - test/db/hsqldb.rb - - test/db/informix.rb - - test/db/jdbc.rb - - test/db/jndi_config.rb - - test/db/logger.rb - - test/db/mssql.rb - - test/db/mysql.rb - - test/db/oracle.rb - - test/db/postgres.rb - - test/db/sqlite3.rb - - test/db2_simple_test.rb - - test/derby_multibyte_test.rb - - test/derby_simple_test.rb - - test/generic_jdbc_connection_test.rb - - test/h2_simple_test.rb - - test/has_many_through.rb - - test/hsqldb_simple_test.rb - - test/informix_simple_test.rb - - test/jdbc_adapter/jdbc_db2_test.rb - - test/jdbc_adapter/jdbc_sybase_test.rb - - test/jdbc_common.rb - - test/jndi_callbacks_test.rb - - test/jndi_test.rb - - test/manualTestDatabase.rb - - test/minirunit/testConnect.rb - - test/minirunit/testH2.rb - - test/minirunit/testHsqldb.rb - - test/minirunit/testLoadActiveRecord.rb - - test/minirunit/testMysql.rb - - test/minirunit/testRawSelect.rb - - test/minirunit.rb - - test/models/add_not_null_column_to_table.rb - - test/models/auto_id.rb - - test/models/data_types.rb - - test/models/entry.rb - - test/models/reserved_word.rb - - test/mssql_simple_test.rb - - test/mysql_multibyte_test.rb - - test/mysql_simple_test.rb - - test/oracle_simple_test.rb - - test/postgres_reserved_test.rb - - test/postgres_simple_test.rb - - test/simple.rb - - test/sqlite3_simple_test.rb - - lib/jdbc_adapter/jdbc.rake - - src/java/jdbc_adapter/JdbcAdapterInternalService.java - - src/java/jdbc_adapter/JdbcConnectionFactory.java - - src/java/jdbc_adapter/JdbcDerbySpec.java - - src/java/jdbc_adapter/JdbcMySQLSpec.java - - src/java/jdbc_adapter/SQLBlock.java -has_rdoc: true -homepage: http://jruby-extras.rubyforge.org/activerecord-jdbc-adapter -licenses: [] - -post_install_message: -rdoc_options: - - --main - - README.txt -require_paths: - - lib -required_ruby_version: !ruby/object:Gem::Requirement - requirements: - - - ">=" - - !ruby/object:Gem::Version - segments: - - 0 - version: "0" -required_rubygems_version: !ruby/object:Gem::Requirement - requirements: - - - ">=" - - !ruby/object:Gem::Version - segments: - - 0 - version: "0" -requirements: [] - -rubyforge_project: jruby-extras -rubygems_version: 1.3.6 -signing_key: -specification_version: 3 -summary: JDBC adapter for ActiveRecord, for use within JRuby on Rails. -test_files: [] - diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-0.9.0.1/History.txt b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-0.9.0.1/History.txt deleted file mode 100755 index 71edb52f98d..00000000000 --- a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-0.9.0.1/History.txt +++ /dev/null @@ -1,169 +0,0 @@ -== 0.9 - -- Now updated to support ActiveRecord 2.2. JNDI-based connections will - automatically connect/disconnect for every AR connection pool - checkout/checkin. For best results, set your pool: parameter >= the - actual maximum size of the JNDI connection pool. (We'll look at how - to eliminate the need to configure AR's pool in the future.) -- NEW! Informix support courtesy of Javier Fernandez-Ivern. -- Backport another Oracle CLOB issue, thanks Edson César. -- Rubyforge #22018: chomp final trailing semicolon for oracle -- JRUBY-2848: Fix NPE error in set_native_database_types -- Rework oracle lob saving callback to be Rails 2.1 friendly (assist - from court3nay) -- JRUBY-2715: Add create/drop database methods to Postgres (Peter Williams) -- JRUBY-3183: Fix structure dump for Postgres (Ryan Bell) -- JRUBY-3184: recreate_database for test database working for PG (Ryan Bell) -- JRUBY-3186: disable referential integrity for PG (Ryan Bell) -- Authoritative repository now hosted at - git://github.com/nicksieger/activerecord-jdbc-adapter.git; rubyforge - svn trunk cleaned out. - -== 0.8.2 - -- Added an optional config key called :dialect. Using :dialect allows you to - override the default SQL dialect for the driver class being used. There are - a few cases for this: - - Using using Sybase w/ the jTDS driver. - - Using rebranded drivers. - - It makes more sense to use :dialect, rather then :driver when using JNDI. -- JRUBY-2619: Typo with :test config causing problems with dev database (Igor Minar) -- 20524, JRUBY-2612: Since when did I think that there was a #true? method on Object? - -== 0.8.1 - -- Now sporting a JDBC sqlite3 adapter! Thanks Joseph Athman. -- Added support for InterSystems Cache database (Ryan Bell) -- Fix for JRUBY-2256 -- JRUBY-1638, JRUBY-2404, JRUBY-2463: schema.table handling and Oracle NUMBER fixes (Darcy Schultz & Jesse Hu) -- Add structure dump and other DDL-ish for DB2 (courtesy abedra and stuarthalloway) -- Fix missing quote_table_name function under Rails 1.2.6 and earlier -- Small tweaks to jdbc.rake to select proper config -- JRUBY-2011: Fix MSSQL string un-quoting issue (Silvio Fonseca) -- JRUBY-1977, 17427: Fix information_schema select issue with MSSQL (Matt Burke) -- 20479: Improve get_table_name for MSSQL (Aslak Hellesøy) -- 20243: numerics improvements for MSSQL (Aslak Hellesøy) -- 20172: don't quote table names for MSSQL (Thor Marius Henrichsen) -- 19729: check for primary key existence in postgres during insert (Martin Luder) -- JRUBY-2297, 18846: retrying failing SQL statements is harmful when not autocommitting (Craig McMillan) -- 10021: very preliminary sybase support. (Mark Atkinson) Not usable until collision w/ sqlserver driver is resolved. -- JRUBY-2312, JRUBY-2319, JRUBY-2322: Oracle timestamping issues (Jesse Hu & Michael König) -- JRUBY-2422: Fix MySQL referential integrity and rollback issues -- JRUBY-2382: mysql string quoting fails with ArrayIndexOutofBoundsException - -== 0.8 - -- NOTE: This release is only compatible with JRuby 1.1RC3 or later. -- Because of recent API changes in trunk in preparation for JRuby 1.1, this release is not - backward compatible with previous JRuby releases. Hence the version bump. -- Internal: convert Java methods to be defined with annotations -- Fix problem with reserved words coming back pre-quoted from #indexes in postgres -- JRUBY-2205: Fix N^2 allocation of bytelists for mysql quoting (taw) -- Attempt a fix for Rubyforge 18059 -- Upgrade derby to 10.3.2.1 -- Fix db:create etc. in the case where JDBC is loaded in Rails' preinitializer.rb -- Fix db:drop to actually work -- Fix for Rubyforge #11567 (Matt Williams) - -== 0.7.2 - -- JRUBY-1905: add_column for derby, hsqldb, and postgresql (Stephen Bannasch) -- Fix db:create for JDBC -- Support Rails 2 with the old "require 'jdbc_adapter'" approach -- JRUBY-1966: Instead of searching for just tables, search for views and tables. -- JRUBY-1583: DB2 numeric quoting (Ryan Shillington) -- JRUBY-1634: Oracle DATE type mapping (Daniel Wintschel) -- JRUBY-1543: rename_column issue with more recent MySQL drivers (Oliver Schmelzle) -- Rubyforge #15074: ConnectionAdapters::JdbcAdapter.indexes is missing name and - schema_name parameters in the method signature (Igor Minar) -- Rubyforge #13558: definition for the indexes method (T Meyarivan) -- JRUBY-2051: handle schemaname and tablename more correctly for columns -- JRUBY-2102: Postgres Adapter cannot handle datetime type (Rainer Hahnekamp) -- JRUBY-2018: Oracle behind ActiveRecord-JDBC fails with "Invalid column index" (K Venkatasubramaniyan) -- JRUBY-2012: jdbc_mysql structure dump fails for mysql views (Tyler Jennings) - -== 0.7.1 - -- Add adapter and driver for H2 courtesy of Caleb Land -- Fix "undefined method `last' for {}:Hash" error introduced with new Rake 0.8.1 (JRUBY-1859) - -== 0.7 - -- PLEASE NOTE: This release is not compatible with JRuby releases earlier than - 1.0.3 or 1.1b2. If you must use JRuby 1.0.2 or earlier, please install the - 0.6 release. -- Release coincides with JRuby 1.0.3 and JRuby 1.1b2 releases -- Simultaneous support for JRuby trunk and 1.0 branch -- Get rid of log_no_bench method, so we time SQL execution again. -- Implement #select_rows -- MySQL migration and quoting updates - -== 0.6 - -- Gem is renamed to "activerecord-jdbc-adapter" to follow new conventions - introduced in Rails 2.0 for third-party adapters. Rails 2.0 compatibility is - introduced. -- Add dependency on ActiveRecord >= 1.14 (from the Rails 1.1.x release) -- New drivers (jdbc-XXX) and adapter (activerecord-jdbcXXX-adapter) gems - available separately. See the README.txt file for details. -- Plain "jdbc" driver is still available if you want to use the full - driver/url way of specifying the driver. -- More bugfixes to Oracle and SQLServer courtesy of Ola & ThoughtWorks - -== 0.5 - -- Release coincides with JRuby 1.0.1 release -- It is no longer necessary to specify :driver and :url configuration - parameters for the mysql, postgresql, oracle, derby, hsqldb, and h2 - adapters. The previous configuration is still valid and compatible, but for - new applications, this makes it possible to use the exact same database.yml - configuration as Rails applications running under native Ruby. -- JDBC drivers can now be dynamically loaded by Ruby code, without being on - the classpath prior to launching JRuby. Simply use "require - 'jdbc-driver.jar'" in JRuby code to add it to the runtime classpath. -- Updates to HSQL, MS SQLServer, Postgres, Oracle and Derby adapters - -== 0.4 - -- Release coincides with JRuby 1.0 release -- Shoring up PostgreSQL (courtesy Dudley Flanders) and HSQL (courtesy Matthew - Williams) -- Fix timestamps on Oracle to use DATE (as everything else) -- Derby fixes: Fix for open result set issue, better structure dump, quoting, - column type changing -- Sybase type recognition fix (courtesy Dean Mao) - -== 0.3.1 - -- Derby critical fixes shortly after 0.3 - -== 0.3 - -- Release coincides with JRuby 1.0.0RC1 release -- Improvements for Derby, Postgres, and Oracle, all of which are running - > 95% of AR tests - -== 0.2.4 - -- Release coincides with JRuby 0.9.9 release -- JRuby 0.9.9 is required -- MySQL close to 100% working -- Derby improvements -- DECIMAL/NUMERIC/FLOAT/REAL bugs fixed with type recognition for Oracle, - Postgres, etc. -- HSQLDB has regressed this release and may not be functioning; we'll get it - fixed for the next one - -== 0.2.3 - -- Release coincides (and compatible) with JRuby 0.9.8 release -- 8 bugs fixed: see http://rubyurl.com/0Da -- Improvements and compatibility fixes for Rails 1.2.x - -== 0.2.1, 0.2.2 - -- Early releases, added better support for multiple databases - -== 0.0.1 - -- Initial, very alpha release diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-0.9.0.1/Manifest.txt b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-0.9.0.1/Manifest.txt deleted file mode 100755 index 09c0f242b4e..00000000000 --- a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-0.9.0.1/Manifest.txt +++ /dev/null @@ -1,93 +0,0 @@ -History.txt -Manifest.txt -README.txt -Rakefile -LICENSE.txt -lib/active_record/connection_adapters/cachedb_adapter.rb -lib/active_record/connection_adapters/derby_adapter.rb -lib/active_record/connection_adapters/h2_adapter.rb -lib/active_record/connection_adapters/hsqldb_adapter.rb -lib/active_record/connection_adapters/informix_adapter.rb -lib/active_record/connection_adapters/jdbc_adapter.rb -lib/active_record/connection_adapters/jdbc_adapter_spec.rb -lib/active_record/connection_adapters/jndi_adapter.rb -lib/active_record/connection_adapters/mysql_adapter.rb -lib/active_record/connection_adapters/oracle_adapter.rb -lib/active_record/connection_adapters/postgresql_adapter.rb -lib/active_record/connection_adapters/sqlite3_adapter.rb -lib/jdbc_adapter/jdbc_cachedb.rb -lib/jdbc_adapter/jdbc_db2.rb -lib/jdbc_adapter/jdbc_derby.rb -lib/jdbc_adapter/jdbc_firebird.rb -lib/jdbc_adapter/jdbc_hsqldb.rb -lib/jdbc_adapter/jdbc_informix.rb -lib/jdbc_adapter/jdbc_mimer.rb -lib/jdbc_adapter/jdbc_mssql.rb -lib/jdbc_adapter/jdbc_mysql.rb -lib/jdbc_adapter/jdbc_oracle.rb -lib/jdbc_adapter/jdbc_postgre.rb -lib/jdbc_adapter/jdbc_sqlite3.rb -lib/jdbc_adapter/jdbc_sybase.rb -lib/jdbc_adapter/missing_functionality_helper.rb -lib/jdbc_adapter/rake_tasks.rb -lib/jdbc_adapter/tsql_helper.rb -lib/jdbc_adapter/version.rb -lib/jdbc_adapter.rb -lib/jdbc_adapter/jdbc_adapter_internal.jar -test/activerecord/connection_adapters/type_conversion_test.rb -test/activerecord/connections/native_jdbc_mysql/connection.rb -test/cachedb_simple_test.rb -test/db/cachedb.rb -test/db/db2.rb -test/db/derby.rb -test/db/h2.rb -test/db/hsqldb.rb -test/db/informix.rb -test/db/jdbc.rb -test/db/jndi_config.rb -test/db/logger.rb -test/db/mssql.rb -test/db/mysql.rb -test/db/oracle.rb -test/db/postgres.rb -test/db/sqlite3.rb -test/db2_simple_test.rb -test/derby_multibyte_test.rb -test/derby_simple_test.rb -test/generic_jdbc_connection_test.rb -test/h2_simple_test.rb -test/has_many_through.rb -test/hsqldb_simple_test.rb -test/informix_simple_test.rb -test/jdbc_adapter/jdbc_db2_test.rb -test/jdbc_adapter/jdbc_sybase_test.rb -test/jdbc_common.rb -test/jndi_callbacks_test.rb -test/jndi_test.rb -test/manualTestDatabase.rb -test/minirunit/testConnect.rb -test/minirunit/testH2.rb -test/minirunit/testHsqldb.rb -test/minirunit/testLoadActiveRecord.rb -test/minirunit/testMysql.rb -test/minirunit/testRawSelect.rb -test/minirunit.rb -test/models/add_not_null_column_to_table.rb -test/models/auto_id.rb -test/models/data_types.rb -test/models/entry.rb -test/models/reserved_word.rb -test/mssql_simple_test.rb -test/mysql_multibyte_test.rb -test/mysql_simple_test.rb -test/oracle_simple_test.rb -test/postgres_reserved_test.rb -test/postgres_simple_test.rb -test/simple.rb -test/sqlite3_simple_test.rb -lib/jdbc_adapter/jdbc.rake -src/java/jdbc_adapter/JdbcAdapterInternalService.java -src/java/jdbc_adapter/JdbcConnectionFactory.java -src/java/jdbc_adapter/JdbcDerbySpec.java -src/java/jdbc_adapter/JdbcMySQLSpec.java -src/java/jdbc_adapter/SQLBlock.java diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-0.9.0.1/README.txt b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-0.9.0.1/README.txt deleted file mode 100755 index a263ae1195d..00000000000 --- a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-0.9.0.1/README.txt +++ /dev/null @@ -1,148 +0,0 @@ -activerecord-jdbc-adapter is a database adapter for Rails' ActiveRecord -component that can be used with JRuby[http://www.jruby.org/]. It allows use of -virtually any JDBC-compliant database with your JRuby on Rails application. - -== Databases - -What's there, and what is not there: - -* MySQL - Complete support -* PostgreSQL - Complete support -* Oracle - Complete support -* Microsoft SQL Server - Complete support except for change_column_default -* DB2 - Complete, except for the migrations: - * change_column - * change_column_default - * remove_column - * rename_column - * add_index - * remove_index - * rename_table -* FireBird - Complete, except for change_column_default and rename_column -* Derby - Complete, except for: - * change_column - * change_column_default - * remove_column - * rename_column -* HSQLDB - Complete -* H2 - Complete -* SQLite3 - work in progress -* Informix - Fairly complete support, all tests pass and migrations appear to work. Comments welcome. - -Other databases will require testing and likely a custom configuration module. -Please join the jruby-extras -mailing-list[http://rubyforge.org/mail/?group_id=2014] to help us discover -support for more databases. - -== Using ActiveRecord JDBC - -=== Inside Rails - -To use activerecord-jdbc-adapter with JRuby on Rails: - -1. Choose the adapter you wish to gem install. The following pre-packaged - adapters are available: - - * base jdbc (<tt>activerecord-jdbc-adapter</tt>). Supports all available databases via JDBC, but requires you to download and manually install the database vendor's JDBC driver .jar file. - * mysql (<tt>activerecord-jdbcmysql-adapter</tt>) - * postgresql (<tt>activerecord-jdbcpostgresql-adapter</tt>) - * derby (<tt>activerecord-jdbcderby-adapter</tt>) - * hsqldb (<tt>activerecord-jdbchsqldb-adapter</tt>) - * h2 (<tt>activerecord-jdbch2-adapter</tt>) - -2. If you're using Rails 2.0, you may skip to the next step. For Rails prior to - version 2.0, you'll need to add one-time setup to your config/environment.rb - file in your Rails application. Add the following lines just before the - <code>Rails::Initializer</code>. (If you're using activerecord-jdbc-adapter - under the old gem name used in versions 0.5 and earlier (ActiveRecord-JDBC), - replace 'activerecord-jdbc-adapter' with 'ActiveRecord-JDBC' below.) - - if RUBY_PLATFORM =~ /java/ - require 'rubygems' - gem 'activerecord-jdbc-adapter' - require 'jdbc_adapter' - end - -3. Configure your database.yml to use the <code>jdbc</code> adapter. For mysql, - postgres, derby, oracle, hsqldb, h2, and informix you can simply configure - the database in the normal Rails style. If you use one of the convenience - 'activerecord-jdbcXXX-adapter' adapters, be sure and put a 'jdbc' prefix in - front of the databas adapter name as below. - - development: - adapter: jdbcmysql - username: blog - password: - hostname: localhost - database: weblog_development - -For other databases, you'll need to know the database driver class and URL. -Example: - - development: - adapter: jdbc - username: blog - password: - driver: com.mysql.jdbc.Driver - url: jdbc:mysql://localhost:3306/weblog_development - -=== Standalone, with ActiveRecord - -1. Install the gem with JRuby: - - jruby -S gem install activerecord-jdbc-adapter - - If you wish to use the adapter for a specific database, you can install it - directly and a driver gem will be installed as well: - - jruby -S gem install activerecord-jdbcderby-adapter - -2. If using ActiveRecord 2.0 (Rails 2.0) or greater, you can skip to the next - step. Otherwise, ensure the following code gets executed in your script: - - require 'rubygems' - gem 'activerecord-jdbc-adapter' - require 'jdbc_adapter' - require 'active_record' - -3. After this you can establish a JDBC connection like this: - - ActiveRecord::Base.establish_connection( - :adapter => 'jdbcderby', - :database => "db/my-database" - ) - - or like this (but requires that you manually put the driver jar on the classpath): - - ActiveRecord::Base.establish_connection( - :adapter => 'jdbc', - :driver => 'org.apache.derby.jdbc.EmbeddedDriver', - :url => 'jdbc:derby:test_ar;create=true' - ) - -== Getting the source - -The source for activerecord-jdbc-adapter is available using git. - - git clone git://github.com/nicksieger/activerecord-jdbc-adapter.git - -== Running AR-JDBC's Tests - -Drivers for 4 open-source databases are included. Provided you have MySQL -installed, you can simply type <tt>jruby -S rake</tt> to run the tests. A -database named <tt>weblog_development</tt> is needed beforehand with a -connection user of "blog" and password empty. - -== Authors - -This project was written by Nick Sieger <nick@nicksieger.com> and Ola Bini -<olabini@gmail.com> with lots of help from the JRuby community. - -== License - -activerecord-jdbc-adapter is released under a BSD license. See the LICENSE file -included with the distribution for details. - -Open-source driver gems for activerecord-jdbc-adapter are licensed under the -same license the database's drivers are licensed. See each driver gem's -LICENSE.txt file for details. diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-0.9.0.1/Rakefile b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-0.9.0.1/Rakefile deleted file mode 100755 index e7d1091b9fc..00000000000 --- a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-0.9.0.1/Rakefile +++ /dev/null @@ -1,179 +0,0 @@ -require 'rake' -require 'rake/testtask' - -task :default => [:java_compile, :test] - -def java_classpath_arg # myriad of ways to discover JRuby classpath - begin - cpath = Java::java.lang.System.getProperty('java.class.path').split(File::PATH_SEPARATOR) - cpath += Java::java.lang.System.getProperty('sun.boot.class.path').split(File::PATH_SEPARATOR) - jruby_cpath = cpath.compact.join(File::PATH_SEPARATOR) - rescue => e - end - unless jruby_cpath - jruby_cpath = ENV['JRUBY_PARENT_CLASSPATH'] || ENV['JRUBY_HOME'] && - FileList["#{ENV['JRUBY_HOME']}/lib/*.jar"].join(File::PATH_SEPARATOR) - end - jruby_cpath ? "-cp \"#{jruby_cpath}\"" : "" -end - -desc "Compile the native Java code." -task :java_compile do - pkg_classes = File.join(*%w(pkg classes)) - jar_name = File.join(*%w(lib jdbc_adapter jdbc_adapter_internal.jar)) - mkdir_p pkg_classes - sh "javac -target 1.5 -source 1.5 -d pkg/classes #{java_classpath_arg} #{FileList['src/java/**/*.java'].join(' ')}" - sh "jar cf #{jar_name} -C #{pkg_classes} ." -end -file "lib/jdbc_adapter/jdbc_adapter_internal.jar" => :java_compile - -task :filelist do - puts FileList['pkg/**/*'].inspect -end - -if RUBY_PLATFORM =~ /java/ - # TODO: add more databases into the standard tests here. - task :test => [:test_mysql, :test_jdbc, :test_derby, :test_hsqldb, :test_h2, :test_sqlite3] -else - task :test => [:test_mysql] -end - -FileList['drivers/*'].each do |d| - next unless File.directory?(d) - driver = File.basename(d) - Rake::TestTask.new("test_#{driver}") do |t| - files = FileList["test/#{driver}*test.rb"] - if driver == "derby" - files << 'test/activerecord/connection_adapters/type_conversion_test.rb' - end - t.ruby_opts << "-rjdbc/#{driver}" - t.test_files = files - t.libs << "test" << "#{d}/lib" - end -end - -Rake::TestTask.new(:test_jdbc) do |t| - t.test_files = FileList['test/generic_jdbc_connection_test.rb', 'test/jndi_callbacks_test.rb'] - t.libs << 'test' << 'drivers/mysql/lib' -end - -Rake::TestTask.new(:test_jndi) do |t| - t.test_files = FileList['test/jndi_test.rb'] - t.libs << 'test' << 'drivers/derby/lib' -end - -task :test_postgresql => [:test_postgres] -task :test_pgsql => [:test_postgres] - -# Ensure oracle driver is on your classpath before launching rake -Rake::TestTask.new(:test_oracle) do |t| - t.test_files = FileList['test/oracle_simple_test.rb'] - t.libs << 'test' -end - -# Ensure DB2 driver is on your classpath before launching rake -Rake::TestTask.new(:test_db2) do |t| - t.test_files = FileList['test/db2_simple_test.rb'] - t.libs << 'test' -end - -# Ensure InterSystems CacheDB driver is on your classpath before launching rake -Rake::TestTask.new(:test_cachedb) do | t | - t.test_files = FileList[ 'test/cachedb_simple_test.rb' ] - t.libs << 'test' -end - -# Ensure that the jTDS driver in on your classpath before launching rake -Rake::TestTask.new(:test_mssql) do | t | - t.test_files = FileList[ 'test/mssql_simple_test.rb' ] - t.libs << 'test' -end - -# Ensure that the Informix driver is on your classpath before launching rake -Rake::TestTask.new(:test_informix) do |t| - t.test_files = FileList[ 'test/informix_simple_test.rb' ] - t.libs << 'test' -end - -# Tests for JDBC adapters that don't require a database. -Rake::TestTask.new(:test_jdbc_adapters) do | t | - t.test_files = FileList[ 'test/jdbc_adapter/jdbc_sybase_test.rb' ] - t.libs << 'test' -end - -MANIFEST = FileList["History.txt", "Manifest.txt", "README.txt", - "Rakefile", "LICENSE.txt", "lib/**/*.rb", "lib/jdbc_adapter/jdbc_adapter_internal.jar", "test/**/*.rb", - "lib/**/*.rake", "src/**/*.java"] - -file "Manifest.txt" => :manifest -task :manifest do - File.open("Manifest.txt", "w") {|f| MANIFEST.each {|n| f << "#{n}\n"} } -end -Rake::Task['manifest'].invoke # Always regen manifest, so Hoe has up-to-date list of files - -require File.dirname(__FILE__) + "/lib/jdbc_adapter/version" -begin - require 'hoe' - Hoe.new("activerecord-jdbc-adapter", JdbcAdapter::Version::VERSION) do |p| - p.rubyforge_name = "jruby-extras" - p.url = "http://jruby-extras.rubyforge.org/activerecord-jdbc-adapter" - p.author = "Nick Sieger, Ola Bini and JRuby contributors" - p.email = "nick@nicksieger.com, ola.bini@gmail.com" - p.summary = "JDBC adapter for ActiveRecord, for use within JRuby on Rails." - p.changes = p.paragraphs_of('History.txt', 0..1).join("\n\n") - p.description = p.paragraphs_of('README.txt', 0...1).join("\n\n") - end.spec.dependencies.delete_if { |dep| dep.name == "hoe" } -rescue LoadError - puts "You really need Hoe installed to be able to package this gem" -rescue => e - puts "ignoring error while loading hoe: #{e.to_s}" -end - -def rake(*args) - ruby "-S", "rake", *args -end - -%w(test package install_gem release clean).each do |task| - desc "Run rake #{task} on all available adapters and drivers" - task "all:#{task}" => task -end - -(Dir["drivers/*/Rakefile"] + Dir["adapters/*/Rakefile"]).each do |rakefile| - dir = File.dirname(rakefile) - prefix = dir.sub(%r{/}, ':') - tasks = %w(package install_gem debug_gem clean) - tasks << "test" if File.directory?(File.join(dir, "test")) - tasks.each do |task| - desc "Run rake #{task} on #{dir}" - task "#{prefix}:#{task}" do - Dir.chdir(dir) do - rake task - end - end - task "#{File.dirname(dir)}:#{task}" => "#{prefix}:#{task}" - task "all:#{task}" => "#{prefix}:#{task}" - end - desc "Run rake release on #{dir}" - task "#{prefix}:release" do - Dir.chdir(dir) do - version = nil - if dir =~ /adapters/ - version = ENV['VERSION'] - else - Dir["lib/**/*.rb"].each do |file| - version ||= File.open(file) {|f| f.read =~ /VERSION = "([^"]+)"/ && $1} - end - end - rake "release", "VERSION=#{version}" - end - end - # Only release adapters synchronously with main release. Drivers are versioned - # according to their JDBC driver versions. - if dir =~ /adapters/ - task "adapters:release" => "#{prefix}:release" - task "all:release" => "#{prefix}:release" - end -end - -require 'rake/clean' -CLEAN.include 'derby*', 'test.db.*','test/reports', 'test.sqlite3','lib/**/*.jar','manifest.mf', '*.log' diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-0.9.0.1/lib/active_record/connection_adapters/cachedb_adapter.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-0.9.0.1/lib/active_record/connection_adapters/cachedb_adapter.rb deleted file mode 100755 index 71d7c37f4b8..00000000000 --- a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-0.9.0.1/lib/active_record/connection_adapters/cachedb_adapter.rb +++ /dev/null @@ -1 +0,0 @@ -require 'active_record/connection_adapters/jdbc_adapter' diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-0.9.0.1/lib/active_record/connection_adapters/derby_adapter.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-0.9.0.1/lib/active_record/connection_adapters/derby_adapter.rb deleted file mode 100755 index 4c0b4aaf9c7..00000000000 --- a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-0.9.0.1/lib/active_record/connection_adapters/derby_adapter.rb +++ /dev/null @@ -1,13 +0,0 @@ -tried_gem = false -begin - require "jdbc/derby" -rescue LoadError - unless tried_gem - require 'rubygems' - gem "jdbc-derby" - tried_gem = true - retry - end - # trust that the derby jar is already present -end -require 'active_record/connection_adapters/jdbc_adapter'
\ No newline at end of file diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-0.9.0.1/lib/active_record/connection_adapters/h2_adapter.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-0.9.0.1/lib/active_record/connection_adapters/h2_adapter.rb deleted file mode 100755 index 4a4b53dc2cf..00000000000 --- a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-0.9.0.1/lib/active_record/connection_adapters/h2_adapter.rb +++ /dev/null @@ -1 +0,0 @@ -require 'active_record/connection_adapters/jdbc_adapter'
\ No newline at end of file diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-0.9.0.1/lib/active_record/connection_adapters/hsqldb_adapter.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-0.9.0.1/lib/active_record/connection_adapters/hsqldb_adapter.rb deleted file mode 100755 index 9aacee7fd4e..00000000000 --- a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-0.9.0.1/lib/active_record/connection_adapters/hsqldb_adapter.rb +++ /dev/null @@ -1,13 +0,0 @@ -tried_gem = false -begin - require "jdbc/hsqldb" -rescue LoadError - unless tried_gem - require 'rubygems' - gem "jdbc-hsqldb" - tried_gem = true - retry - end - # trust that the hsqldb jar is already present -end -require 'active_record/connection_adapters/jdbc_adapter'
\ No newline at end of file diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-0.9.0.1/lib/active_record/connection_adapters/informix_adapter.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-0.9.0.1/lib/active_record/connection_adapters/informix_adapter.rb deleted file mode 100755 index 71d7c37f4b8..00000000000 --- a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-0.9.0.1/lib/active_record/connection_adapters/informix_adapter.rb +++ /dev/null @@ -1 +0,0 @@ -require 'active_record/connection_adapters/jdbc_adapter' diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-0.9.0.1/lib/active_record/connection_adapters/jdbc_adapter.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-0.9.0.1/lib/active_record/connection_adapters/jdbc_adapter.rb deleted file mode 100755 index 3e0a6d3b941..00000000000 --- a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-0.9.0.1/lib/active_record/connection_adapters/jdbc_adapter.rb +++ /dev/null @@ -1,645 +0,0 @@ -require 'active_record/connection_adapters/abstract_adapter' -require 'java' -require 'active_record/connection_adapters/jdbc_adapter_spec' -require 'jdbc_adapter/jdbc_adapter_internal' -require 'bigdecimal' - -begin - require 'jdbc_adapter/rake_tasks' -rescue LoadError -end if defined?(RAILS_ROOT) - -module ActiveRecord - module ConnectionAdapters # :nodoc: - module SchemaStatements - # The original implementation of this had a bug, which modifies native_database_types. - # This version allows us to cache that value. - def type_to_sql(type, limit = nil, precision = nil, scale = nil) #:nodoc: - native = native_database_types[type.to_s.downcase.to_sym] - column_type_sql = native.is_a?(Hash) ? native[:name] : native - if type == :decimal # ignore limit, use precison and scale - precision ||= native[:precision] - scale ||= native[:scale] - if precision - if scale - column_type_sql += "(#{precision},#{scale})" - else - column_type_sql += "(#{precision})" - end - else - raise ArgumentError, "Error adding decimal column: precision cannot be empty if scale if specified" if scale - end - column_type_sql - else - limit ||= native[:limit] - column_type_sql += "(#{limit})" if limit - column_type_sql - end - end - end - end -end - -module JdbcSpec - module ActiveRecordExtensions - def jdbc_connection(config) - connection = ::ActiveRecord::ConnectionAdapters::JdbcConnection.new(config) - ::ActiveRecord::ConnectionAdapters::JdbcAdapter.new(connection, logger, config) - end - alias jndi_connection jdbc_connection - - def embedded_driver(config) - config[:username] ||= "sa" - config[:password] ||= "" - jdbc_connection(config) - end - end -end - -module ActiveRecord - class Base - extend JdbcSpec::ActiveRecordExtensions - - alias :attributes_with_quotes_pre_oracle :attributes_with_quotes - def attributes_with_quotes(include_primary_key = true, *args) #:nodoc: - aq = attributes_with_quotes_pre_oracle(include_primary_key, *args) - if connection.class == ConnectionAdapters::JdbcAdapter && (connection.is_a?(JdbcSpec::Oracle) || connection.is_a?(JdbcSpec::Mimer)) - aq[self.class.primary_key] = "?" if include_primary_key && aq[self.class.primary_key].nil? - end - aq - end - end - - module ConnectionAdapters - module Java - Class = java.lang.Class - URL = java.net.URL - URLClassLoader = java.net.URLClassLoader - end - - module Jdbc - Mutex = java.lang.Object.new - DriverManager = java.sql.DriverManager - Statement = java.sql.Statement - Types = java.sql.Types - - # some symbolic constants for the benefit of the JDBC-based - # JdbcConnection#indexes method - module IndexMetaData - INDEX_NAME = 6 - NON_UNIQUE = 4 - TABLE_NAME = 3 - COLUMN_NAME = 9 - end - - module TableMetaData - TABLE_CAT = 1 - TABLE_SCHEM = 2 - TABLE_NAME = 3 - TABLE_TYPE = 4 - end - - module PrimaryKeyMetaData - COLUMN_NAME = 4 - end - - end - - # I want to use JDBC's DatabaseMetaData#getTypeInfo to choose the best native types to - # use for ActiveRecord's Adapter#native_database_types in a database-independent way, - # but apparently a database driver can return multiple types for a given - # java.sql.Types constant. So this type converter uses some heuristics to try to pick - # the best (most common) type to use. It's not great, it would be better to just - # delegate to each database's existin AR adapter's native_database_types method, but I - # wanted to try to do this in a way that didn't pull in all the other adapters as - # dependencies. Suggestions appreciated. - class JdbcTypeConverter - # The basic ActiveRecord types, mapped to an array of procs that are used to #select - # the best type. The procs are used as selectors in order until there is only one - # type left. If all the selectors are applied and there is still more than one - # type, an exception will be raised. - AR_TO_JDBC_TYPES = { - :string => [ lambda {|r| Jdbc::Types::VARCHAR == r['data_type'].to_i}, - lambda {|r| r['type_name'] =~ /^varchar/i}, - lambda {|r| r['type_name'] =~ /^varchar$/i}, - lambda {|r| r['type_name'] =~ /varying/i}], - :text => [ lambda {|r| [Jdbc::Types::LONGVARCHAR, Jdbc::Types::CLOB].include?(r['data_type'].to_i)}, - lambda {|r| r['type_name'] =~ /^text$/i}, # For Informix - lambda {|r| r['type_name'] =~ /^(text|clob)$/i}, - lambda {|r| r['type_name'] =~ /^character large object$/i}, - lambda {|r| r['sql_data_type'] == 2005}], - :integer => [ lambda {|r| Jdbc::Types::INTEGER == r['data_type'].to_i}, - lambda {|r| r['type_name'] =~ /^integer$/i}, - lambda {|r| r['type_name'] =~ /^int4$/i}, - lambda {|r| r['type_name'] =~ /^int$/i}], - :decimal => [ lambda {|r| Jdbc::Types::DECIMAL == r['data_type'].to_i}, - lambda {|r| r['type_name'] =~ /^decimal$/i}, - lambda {|r| r['type_name'] =~ /^numeric$/i}, - lambda {|r| r['type_name'] =~ /^number$/i}, - lambda {|r| r['type_name'] =~ /^real$/i}, - lambda {|r| r['precision'] == '38'}, - lambda {|r| r['data_type'] == '2'}], - :float => [ lambda {|r| [Jdbc::Types::FLOAT,Jdbc::Types::DOUBLE, Jdbc::Types::REAL].include?(r['data_type'].to_i)}, - lambda {|r| r['data_type'].to_i == Jdbc::Types::REAL}, #Prefer REAL to DOUBLE for Postgresql - lambda {|r| r['type_name'] =~ /^float/i}, - lambda {|r| r['type_name'] =~ /^double$/i}, - lambda {|r| r['type_name'] =~ /^real$/i}, - lambda {|r| r['precision'] == '15'}], - :datetime => [ lambda {|r| Jdbc::Types::TIMESTAMP == r['data_type'].to_i}, - lambda {|r| r['type_name'] =~ /^datetime$/i}, - lambda {|r| r['type_name'] =~ /^timestamp$/i}, - lambda {|r| r['type_name'] =~ /^date/i}, - lambda {|r| r['type_name'] =~ /^integer/i}], #Num of milliseconds for SQLite3 JDBC Driver - :timestamp => [ lambda {|r| Jdbc::Types::TIMESTAMP == r['data_type'].to_i}, - lambda {|r| r['type_name'] =~ /^timestamp$/i}, - lambda {|r| r['type_name'] =~ /^datetime/i}, - lambda {|r| r['type_name'] =~ /^date/i}, - lambda {|r| r['type_name'] =~ /^integer/i}], #Num of milliseconds for SQLite3 JDBC Driver - :time => [ lambda {|r| Jdbc::Types::TIME == r['data_type'].to_i}, - lambda {|r| r['type_name'] =~ /^time$/i}, - lambda {|r| r['type_name'] =~ /^datetime/i}, # For Informix - lambda {|r| r['type_name'] =~ /^date/i}, - lambda {|r| r['type_name'] =~ /^integer/i}], #Num of milliseconds for SQLite3 JDBC Driver - :date => [ lambda {|r| Jdbc::Types::DATE == r['data_type'].to_i}, - lambda {|r| r['type_name'] =~ /^date$/i}, - lambda {|r| r['type_name'] =~ /^date/i}, - lambda {|r| r['type_name'] =~ /^integer/i}], #Num of milliseconds for SQLite3 JDBC Driver3 - :binary => [ lambda {|r| [Jdbc::Types::LONGVARBINARY,Jdbc::Types::BINARY,Jdbc::Types::BLOB].include?(r['data_type'].to_i)}, - lambda {|r| r['type_name'] =~ /^blob/i}, - lambda {|r| r['type_name'] =~ /sub_type 0$/i}, # For FireBird - lambda {|r| r['type_name'] =~ /^varbinary$/i}, # We want this sucker for Mimer - lambda {|r| r['type_name'] =~ /^binary$/i}, ], - :boolean => [ lambda {|r| [Jdbc::Types::TINYINT].include?(r['data_type'].to_i)}, - lambda {|r| r['type_name'] =~ /^bool/i}, - lambda {|r| r['data_type'] == '-7'}, - lambda {|r| r['type_name'] =~ /^tinyint$/i}, - lambda {|r| r['type_name'] =~ /^decimal$/i}, - lambda {|r| r['type_name'] =~ /^integer$/i}] - } - - def initialize(types) - @types = types - @types.each {|t| t['type_name'] ||= t['local_type_name']} # Sybase driver seems to want 'local_type_name' - end - - def choose_best_types - type_map = {} - @types.each do |row| - name = row['type_name'].downcase - k = name.to_sym - type_map[k] = { :name => name } - type_map[k][:limit] = row['precision'].to_i if row['precision'] - end - - AR_TO_JDBC_TYPES.keys.each do |k| - typerow = choose_type(k) - type_map[k] = { :name => typerow['type_name'].downcase } - case k - when :integer, :string, :decimal - type_map[k][:limit] = typerow['precision'] && typerow['precision'].to_i - when :boolean - type_map[k][:limit] = 1 - end - end - type_map - end - - def choose_type(ar_type) - procs = AR_TO_JDBC_TYPES[ar_type] - types = @types - procs.each do |p| - new_types = types.select(&p) - new_types = new_types.inject([]) do |typs,t| - typs << t unless typs.detect {|el| el['type_name'] == t['type_name']} - typs - end - return new_types.first if new_types.length == 1 - types = new_types if new_types.length > 0 - end - raise "unable to choose type for #{ar_type} from:\n#{types.collect{|t| t['type_name']}.inspect}" - end - end - - class JdbcDriver - def initialize(name) - @name = name - end - - def driver_class - @driver_class ||= begin - driver_class_const = (@name[0...1].capitalize + @name[1..@name.length]).gsub(/\./, '_') - Jdbc::Mutex.synchronized do - unless Jdbc.const_defined?(driver_class_const) - driver_class_name = @name - Jdbc.module_eval do - include_class(driver_class_name) { driver_class_const } - end - end - end - driver_class = Jdbc.const_get(driver_class_const) - raise "You specify a driver for your JDBC connection" unless driver_class - driver_class - end - end - - def load - Jdbc::DriverManager.registerDriver(create) - end - - def connection(url, user, pass) - Jdbc::DriverManager.getConnection(url, user, pass) - rescue - # bypass DriverManager to get around problem with dynamically loaded jdbc drivers - props = java.util.Properties.new - props.setProperty("user", user) - props.setProperty("password", pass) - create.connect(url, props) - end - - def create - driver_class.new - end - end - - class JdbcColumn < Column - attr_writer :limit, :precision - - COLUMN_TYPES = ::JdbcSpec.constants.map{|c| - ::JdbcSpec.const_get c }.select{ |c| - c.respond_to? :column_selector }.map{|c| - c.column_selector }.inject({}) { |h,val| - h[val[0]] = val[1]; h } - - def initialize(config, name, default, *args) - dialect = config[:dialect] || config[:driver] - for reg, func in COLUMN_TYPES - if reg === dialect.to_s - func.call(config,self) - end - end - super(name,default_value(default),*args) - init_column(name, default, *args) - end - - def init_column(*args) - end - - def default_value(val) - val - end - end - - include_class "jdbc_adapter.JdbcConnectionFactory" - - class JdbcConnection - attr_reader :adapter, :connection_factory - - def initialize(config) - @config = config.symbolize_keys! - @config[:retry_count] ||= 5 - @config[:connection_alive_sql] ||= "select 1" - if @config[:jndi] - begin - configure_jndi - rescue => e - warn "JNDI data source unavailable: #{e.message}; trying straight JDBC" - configure_jdbc - end - else - configure_jdbc - end - connection # force the connection to load - set_native_database_types - @stmts = {} - rescue Exception => e - raise "The driver encountered an error: #{e}" - end - - def adapter=(adapt) - @adapter = adapt - @tps = {} - @native_types.each_pair {|k,v| @tps[k] = v.inject({}) {|memo,kv| memo.merge({kv.first => (kv.last.dup rescue kv.last)})}} - adapt.modify_types(@tps) - end - - # Default JDBC introspection for index metadata on the JdbcConnection. - # This is currently used for migrations by JdbcSpec::HSQDLB and JdbcSpec::Derby - # indexes with a little filtering tacked on. - # - # JDBC index metadata is denormalized (multiple rows may be returned for - # one index, one row per column in the index), so a simple block-based - # filter like that used for tables doesn't really work here. Callers - # should filter the return from this method instead. - def indexes(table_name, name = nil, schema_name = nil) - with_connection_retry_guard do |conn| - metadata = conn.getMetaData - begin - unless String === table_name - table_name = table_name.to_s - else - table_name = table_name.dup - end - table_name.upcase! if metadata.storesUpperCaseIdentifiers - table_name.downcase! if metadata.storesLowerCaseIdentifiers - resultset = metadata.getIndexInfo(nil, schema_name, table_name, false, false) - primary_keys = primary_keys(table_name) - indexes = [] - current_index = nil - while resultset.next - index_name = resultset.get_string(Jdbc::IndexMetaData::INDEX_NAME) - next unless index_name - index_name.downcase! - column_name = resultset.get_string(Jdbc::IndexMetaData::COLUMN_NAME).downcase - - next if primary_keys.include? column_name - - # We are working on a new index - if current_index != index_name - current_index = index_name - table_name = resultset.get_string(Jdbc::IndexMetaData::TABLE_NAME).downcase - non_unique = resultset.get_boolean(Jdbc::IndexMetaData::NON_UNIQUE) - - # empty list for column names, we'll add to that in just a bit - indexes << IndexDefinition.new(table_name, index_name, !non_unique, []) - end - - # One or more columns can be associated with an index - indexes.last.columns << column_name - end - resultset.close - indexes - ensure - metadata.close rescue nil - end - end - end - - def jndi_connection? - @jndi_connection - end - - private - def configure_jndi - jndi = @config[:jndi].to_s - ctx = javax.naming.InitialContext.new - ds = ctx.lookup(jndi) - @connection_factory = JdbcConnectionFactory.impl do - ds.connection - end - unless @config[:driver] - @config[:driver] = connection.meta_data.connection.java_class.name - end - @jndi_connection = true - end - - def configure_jdbc - driver = @config[:driver].to_s - user = @config[:username].to_s - pass = @config[:password].to_s - url = @config[:url].to_s - - unless driver && url - raise ::ActiveRecord::ConnectionFailed, "jdbc adapter requires driver class and url" - end - - if driver =~ /mysql/i && url !~ /#{Regexp.quote(JdbcSpec::MySQL::URL_OPTIONS)}/ - div = url =~ /\?/ ? '&' : '?' - url = "#{url}#{div}#{JdbcSpec::MySQL::URL_OPTIONS}" - @config[:url] = url - end - - jdbc_driver = JdbcDriver.new(driver) - jdbc_driver.load - @connection_factory = JdbcConnectionFactory.impl do - jdbc_driver.connection(url, user, pass) - end - end - end - - class JdbcAdapter < AbstractAdapter - module ShadowCoreMethods - def alias_chained_method(meth, feature, target) - if instance_methods.include?("#{meth}_without_#{feature}") - alias_method "#{meth}_without_#{feature}".to_sym, target - else - alias_method meth, target - end - end - end - - module CompatibilityMethods - def self.needed?(base) - !base.instance_methods.include?("quote_table_name") - end - - def quote_table_name(name) - quote_column_name(name) - end - end - - module ConnectionPoolCallbacks - def self.included(base) - base.checkin :on_checkin - base.checkout :on_checkout - end - - def self.needed? - ActiveRecord::Base.respond_to?(:connection_pool) - end - - def on_checkin - # default implementation does nothing - end - - def on_checkout - # default implementation does nothing - end - end - - module JndiConnectionPoolCallbacks - def self.prepare(adapter, conn) - if ActiveRecord::Base.respond_to?(:connection_pool) && conn.jndi_connection? - adapter.extend self - conn.disconnect! # disconnect initial connection in JdbcConnection#initialize - end - end - - def on_checkin - disconnect! - end - - def on_checkout - reconnect! - end - end - - extend ShadowCoreMethods - include CompatibilityMethods if CompatibilityMethods.needed?(self) - include ConnectionPoolCallbacks if ConnectionPoolCallbacks.needed? - - attr_reader :config - - ADAPTER_TYPES = ::JdbcSpec.constants.map{|c| - ::JdbcSpec.const_get c }.select{ |c| - c.respond_to? :adapter_selector }.map{|c| - c.adapter_selector }.inject({}) { |h,val| - h[val[0]] = val[1]; h } - - def initialize(connection, logger, config) - super(connection, logger) - @config = config - dialect = config[:dialect] || config[:driver] - for reg, func in ADAPTER_TYPES - if reg === dialect.to_s - func.call(@config,self) - end - end - connection.adapter = self - JndiConnectionPoolCallbacks.prepare(self, connection) - end - - def modify_types(tp) - tp - end - - def adapter_name #:nodoc: - 'JDBC' - end - - def supports_migrations? - true - end - - def native_database_types #:nodoc: - @connection.native_database_types - end - - def database_name #:nodoc: - @connection.database_name - end - - def native_sql_to_type(tp) - if /^(.*?)\(([0-9]+)\)/ =~ tp - tname = $1 - limit = $2.to_i - ntype = native_database_types - if ntype[:primary_key] == tp - return :primary_key,nil - else - ntype.each do |name,val| - if name == :primary_key - next - end - if val[:name].downcase == tname.downcase && (val[:limit].nil? || val[:limit].to_i == limit) - return name,limit - end - end - end - elsif /^(.*?)/ =~ tp - tname = $1 - ntype = native_database_types - if ntype[:primary_key] == tp - return :primary_key,nil - else - ntype.each do |name,val| - if val[:name].downcase == tname.downcase && val[:limit].nil? - return name,nil - end - end - end - else - return :string,255 - end - return nil,nil - end - - def reconnect! - @connection.reconnect! - @connection - end - - def disconnect! - @connection.disconnect! - end - - def jdbc_select_all(sql, name = nil) - select(sql, name) - end - alias_chained_method :select_all, :query_cache, :jdbc_select_all - - def select_rows(sql, name = nil) - rows = [] - select(sql, name).each {|row| rows << row.values } - rows - end - - def select_one(sql, name = nil) - select(sql, name).first - end - - def execute(sql, name = nil) - log(sql, name) do - _execute(sql,name) - end - end - - # we need to do it this way, to allow Rails stupid tests to always work - # even if we define a new execute method. Instead of mixing in a new - # execute, an _execute should be mixed in. - def _execute(sql, name = nil) - if JdbcConnection::select?(sql) - @connection.execute_query(sql) - elsif JdbcConnection::insert?(sql) - @connection.execute_insert(sql) - else - @connection.execute_update(sql) - end - end - - def jdbc_update(sql, name = nil) #:nodoc: - execute(sql, name) - end - alias_chained_method :update, :query_dirty, :jdbc_update - - def jdbc_insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) - id = execute(sql, name = nil) - id_value || id - end - alias_chained_method :insert, :query_dirty, :jdbc_insert - - def jdbc_columns(table_name, name = nil) - @connection.columns(table_name.to_s) - end - alias_chained_method :columns, :query_cache, :jdbc_columns - - def tables - @connection.tables - end - - def indexes(table_name, name = nil, schema_name = nil) - @connection.indexes(table_name, name, schema_name) - end - - def begin_db_transaction - @connection.begin - end - - def commit_db_transaction - @connection.commit - end - - def rollback_db_transaction - @connection.rollback - end - - def write_large_object(*args) - @connection.write_large_object(*args) - end - - private - def select(sql, name=nil) - execute(sql,name) - end - end - end -end diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-0.9.0.1/lib/active_record/connection_adapters/jdbc_adapter_spec.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-0.9.0.1/lib/active_record/connection_adapters/jdbc_adapter_spec.rb deleted file mode 100755 index 2cabb146ffd..00000000000 --- a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-0.9.0.1/lib/active_record/connection_adapters/jdbc_adapter_spec.rb +++ /dev/null @@ -1,14 +0,0 @@ - -require 'jdbc_adapter/jdbc_mimer' -require 'jdbc_adapter/jdbc_hsqldb' -require 'jdbc_adapter/jdbc_oracle' -require 'jdbc_adapter/jdbc_postgre' -require 'jdbc_adapter/jdbc_mysql' -require 'jdbc_adapter/jdbc_derby' -require 'jdbc_adapter/jdbc_firebird' -require 'jdbc_adapter/jdbc_db2' -require 'jdbc_adapter/jdbc_mssql' -require 'jdbc_adapter/jdbc_cachedb' -require 'jdbc_adapter/jdbc_sqlite3' -require 'jdbc_adapter/jdbc_sybase' -require 'jdbc_adapter/jdbc_informix' diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-0.9.0.1/lib/active_record/connection_adapters/jndi_adapter.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-0.9.0.1/lib/active_record/connection_adapters/jndi_adapter.rb deleted file mode 100755 index 4a4b53dc2cf..00000000000 --- a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-0.9.0.1/lib/active_record/connection_adapters/jndi_adapter.rb +++ /dev/null @@ -1 +0,0 @@ -require 'active_record/connection_adapters/jdbc_adapter'
\ No newline at end of file diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-0.9.0.1/lib/active_record/connection_adapters/mysql_adapter.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-0.9.0.1/lib/active_record/connection_adapters/mysql_adapter.rb deleted file mode 100755 index 3cb9ee281f9..00000000000 --- a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-0.9.0.1/lib/active_record/connection_adapters/mysql_adapter.rb +++ /dev/null @@ -1,13 +0,0 @@ -tried_gem = false -begin - require "jdbc/mysql" -rescue LoadError - unless tried_gem - require 'rubygems' - gem "jdbc-mysql" - tried_gem = true - retry - end - # trust that the mysql jar is already present -end -require 'active_record/connection_adapters/jdbc_adapter' diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-0.9.0.1/lib/active_record/connection_adapters/oracle_adapter.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-0.9.0.1/lib/active_record/connection_adapters/oracle_adapter.rb deleted file mode 100755 index 4a4b53dc2cf..00000000000 --- a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-0.9.0.1/lib/active_record/connection_adapters/oracle_adapter.rb +++ /dev/null @@ -1 +0,0 @@ -require 'active_record/connection_adapters/jdbc_adapter'
\ No newline at end of file diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-0.9.0.1/lib/active_record/connection_adapters/postgresql_adapter.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-0.9.0.1/lib/active_record/connection_adapters/postgresql_adapter.rb deleted file mode 100755 index ab94a2b649b..00000000000 --- a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-0.9.0.1/lib/active_record/connection_adapters/postgresql_adapter.rb +++ /dev/null @@ -1,13 +0,0 @@ -tried_gem = false -begin - require "jdbc/postgres" -rescue LoadError - unless tried_gem - require 'rubygems' - gem "jdbc-postgres" - tried_gem = true - retry - end - # trust that the postgres jar is already present -end -require 'active_record/connection_adapters/jdbc_adapter'
\ No newline at end of file diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-0.9.0.1/lib/active_record/connection_adapters/sqlite3_adapter.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-0.9.0.1/lib/active_record/connection_adapters/sqlite3_adapter.rb deleted file mode 100755 index 1e4dfcc5403..00000000000 --- a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-0.9.0.1/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ /dev/null @@ -1,13 +0,0 @@ -tried_gem = false -begin - require "jdbc/sqlite3" -rescue LoadError - unless tried_gem - require 'rubygems' - gem "jdbc-sqlite3" - tried_gem = true - retry - end - # trust that the sqlite jar is already present -end -require 'active_record/connection_adapters/jdbc_adapter' diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-0.9.0.1/lib/jdbc_adapter/jdbc.rake b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-0.9.0.1/lib/jdbc_adapter/jdbc.rake deleted file mode 100755 index 94d2f76105b..00000000000 --- a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-0.9.0.1/lib/jdbc_adapter/jdbc.rake +++ /dev/null @@ -1,94 +0,0 @@ -def redefine_task(*args, &block) - task_name = Hash === args.first ? args.first.keys[0] : args.first - existing_task = Rake.application.lookup task_name - if existing_task - class << existing_task; public :instance_variable_set; end - existing_task.instance_variable_set "@prerequisites", FileList[] - existing_task.instance_variable_set "@actions", [] - end - task(*args, &block) -end - -namespace :db do - if Rake::Task["db:create"] - redefine_task :create => :environment do - create_database(ActiveRecord::Base.configurations[RAILS_ENV]) - end - - class << self; alias_method :previous_create_database, :create_database; end - def create_database(config) - begin - ActiveRecord::Base.establish_connection(config) - ActiveRecord::Base.connection - rescue - begin - url = config['url'] - if url - if url =~ /^(.*\/)/ - url = $1 - end - end - - ActiveRecord::Base.establish_connection(config.merge({'database' => nil, 'url' => url})) - ActiveRecord::Base.connection.create_database(config['database']) - ActiveRecord::Base.establish_connection(config) - rescue - previous_create_database(config) - end - end - end - - redefine_task :drop => :environment do - config = ActiveRecord::Base.configurations[RAILS_ENV] - begin - ActiveRecord::Base.establish_connection(config) - db = ActiveRecord::Base.connection.database_name - ActiveRecord::Base.connection.drop_database(db) - rescue - drop_database(config) - end - end - end - - namespace :structure do - redefine_task :dump => :environment do - abcs = ActiveRecord::Base.configurations - ActiveRecord::Base.establish_connection(abcs[RAILS_ENV]) - File.open("db/#{RAILS_ENV}_structure.sql", "w+") { |f| f << ActiveRecord::Base.connection.structure_dump } - if ActiveRecord::Base.connection.supports_migrations? - File.open("db/#{RAILS_ENV}_structure.sql", "a") { |f| f << ActiveRecord::Base.connection.dump_schema_information } - end - end - end - - namespace :test do - redefine_task :clone_structure => [ "db:structure:dump", "db:test:purge" ] do - abcs = ActiveRecord::Base.configurations - abcs['test']['pg_params'] = '?allowEncodingChanges=true' if abcs['test']['adapter'] =~ /postgresql/i - ActiveRecord::Base.establish_connection(abcs["test"]) - ActiveRecord::Base.connection.execute('SET foreign_key_checks = 0') if abcs["test"]["adapter"] =~ /mysql/i - IO.readlines("db/#{RAILS_ENV}_structure.sql").join.split(";\n\n").each do |ddl| - ActiveRecord::Base.connection.execute(ddl.chomp(';')) - end - end - - redefine_task :purge => :environment do - abcs = ActiveRecord::Base.configurations - config = abcs['test'].dup - if config['adapter'] =~ /postgresql/i - if config['url'] - db = config['url'][/\/([^\/]*)$/, 1] - config['url'][/\/([^\/]*)$/, 1] if db_name - else - db = config['database'] - config['database'] = 'postgres' - end - ActiveRecord::Base.establish_connection(config) - else - ActiveRecord::Base.establish_connection(config) - db = ActiveRecord::Base.connection.database_name - end - ActiveRecord::Base.connection.recreate_database(db) - end - end -end diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-0.9.0.1/lib/jdbc_adapter/jdbc_adapter_internal.jar b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-0.9.0.1/lib/jdbc_adapter/jdbc_adapter_internal.jar Binary files differdeleted file mode 100755 index 10449a7193a..00000000000 --- a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-0.9.0.1/lib/jdbc_adapter/jdbc_adapter_internal.jar +++ /dev/null diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-0.9.0.1/lib/jdbc_adapter/jdbc_cachedb.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-0.9.0.1/lib/jdbc_adapter/jdbc_cachedb.rb deleted file mode 100755 index 443606ced93..00000000000 --- a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-0.9.0.1/lib/jdbc_adapter/jdbc_cachedb.rb +++ /dev/null @@ -1,33 +0,0 @@ -require 'jdbc_adapter/tsql_helper' - -module ::JdbcSpec - module ActiveRecordExtensions - def cachedb_connection( config ) - config[:port] ||= 1972 - config[:url] ||= "jdbc:Cache://#{config[:host]}:#{config[:port]}/#{ config[:database]}" - config[:driver] ||= "com.intersys.jdbc.CacheDriver" - jdbc_connection( config ) - end - end - - module CacheDB - include TSqlMethods - - def self.column_selector - [ /cache/i, lambda { | cfg, col | col.extend( ::JdbcSpec::CacheDB::Column ) } ] - end - - def self.adapter_selector - [ /cache/i, lambda { | cfg, adapt | adapt.extend( ::JdbcSpec::CacheDB ) } ] - end - - module Column - end - - def create_table(name, options = { }) - super(name, options) - primary_key = options[:primary_key] || "id" - execute "ALTER TABLE #{name} ADD CONSTRAINT #{name}_PK PRIMARY KEY(#{primary_key})" unless options[:id] == false - end - end -end diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-0.9.0.1/lib/jdbc_adapter/jdbc_db2.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-0.9.0.1/lib/jdbc_adapter/jdbc_db2.rb deleted file mode 100755 index 261a06ef401..00000000000 --- a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-0.9.0.1/lib/jdbc_adapter/jdbc_db2.rb +++ /dev/null @@ -1,193 +0,0 @@ -module JdbcSpec - module DB2 - def self.column_selector - [/db2/i, lambda {|cfg,col| - if cfg[:url] =~ /^jdbc:derby:net:/ - col.extend(::JdbcSpec::Derby::Column) - else - col.extend(::JdbcSpec::DB2::Column) - end }] - end - - def self.adapter_selector - [/db2/i, lambda {|cfg,adapt| - if cfg[:url] =~ /^jdbc:derby:net:/ - adapt.extend(::JdbcSpec::Derby) - else - adapt.extend(::JdbcSpec::DB2) - end }] - end - - module Column - def type_cast(value) - return nil if value.nil? || value =~ /^\s*null\s*$/i - case type - when :string then value - when :integer then defined?(value.to_i) ? value.to_i : (value ? 1 : 0) - when :primary_key then defined?(value.to_i) ? value.to_i : (value ? 1 : 0) - when :float then value.to_f - when :datetime then cast_to_date_or_time(value) - when :timestamp then cast_to_time(value) - when :time then cast_to_time(value) - else value - end - end - def cast_to_date_or_time(value) - return value if value.is_a? Date - return nil if value.blank? - guess_date_or_time((value.is_a? Time) ? value : cast_to_time(value)) - end - - def cast_to_time(value) - return value if value.is_a? Time - time_array = ParseDate.parsedate value - time_array[0] ||= 2000; time_array[1] ||= 1; time_array[2] ||= 1; - Time.send(ActiveRecord::Base.default_timezone, *time_array) rescue nil - end - - def guess_date_or_time(value) - (value.hour == 0 and value.min == 0 and value.sec == 0) ? - Date.new(value.year, value.month, value.day) : value - end - end - - def modify_types(tp) - tp[:primary_key] = 'int generated by default as identity (start with 42) primary key' - tp[:string][:limit] = 255 - tp[:integer][:limit] = nil - tp[:boolean][:limit] = nil - tp - end - - def add_limit_offset!(sql, options) - if limit = options[:limit] - offset = options[:offset] || 0 - sql.gsub!(/SELECT/i, 'SELECT B.* FROM (SELECT A.*, row_number() over () AS internal$rownum FROM (SELECT') - sql << ") A ) B WHERE B.internal$rownum > #{offset} AND B.internal$rownum <= #{limit + offset}" - end - end - - def quote_column_name(column_name) - column_name - end - - def quote(value, column = nil) # :nodoc: - if column && column.type == :primary_key - return value.to_s - end - if column && (column.type == :decimal || column.type == :integer) && value - return value.to_s - end - case value - when String - if column && column.type == :binary - "BLOB('#{quote_string(value)}')" - else - "'#{quote_string(value)}'" - end - else super - end - end - - def quote_string(string) - string.gsub(/'/, "''") # ' (for ruby-mode) - end - - def quoted_true - '1' - end - - def quoted_false - '0' - end - - def recreate_database(name) - do_not_drop = ["stmg_dbsize_info","hmon_atm_info","hmon_collection","policy"] - tables.each do |table| - unless do_not_drop.include?(table) - drop_table(table) - end - end - end - - def remove_index(table_name, options = { }) - execute "DROP INDEX #{quote_column_name(index_name(table_name, options))}" - end - - # This method makes tests pass without understanding why. - # Don't use this in production. - def columns(table_name, name = nil) - super.select do |col| - # strip out "magic" columns from DB2 (?) - !/rolename|roleid|create_time|auditpolicyname|auditpolicyid|remarks/.match(col.name) - end - end - - def add_quotes(name) - return name unless name - %Q{"#{name}"} - end - - def strip_quotes(str) - return str unless str - return str unless /^(["']).*\1$/ =~ str - str[1..-2] - end - - def expand_double_quotes(name) - return name unless name && name['"'] - name.gsub(/"/,'""') - end - - - def structure_dump #:nodoc: - definition="" - rs = @connection.connection.meta_data.getTables(nil,nil,nil,["TABLE"].to_java(:string)) - while rs.next - tname = rs.getString(3) - definition << "CREATE TABLE #{tname} (\n" - rs2 = @connection.connection.meta_data.getColumns(nil,nil,tname,nil) - first_col = true - while rs2.next - col_name = add_quotes(rs2.getString(4)); - default = "" - d1 = rs2.getString(13) - default = d1 ? " DEFAULT #{d1}" : "" - - type = rs2.getString(6) - col_size = rs2.getString(7) - nulling = (rs2.getString(18) == 'NO' ? " NOT NULL" : "") - create_col_string = add_quotes(expand_double_quotes(strip_quotes(col_name))) + - " " + - type + - "" + - nulling + - default - if !first_col - create_col_string = ",\n #{create_col_string}" - else - create_col_string = " #{create_col_string}" - end - - definition << create_col_string - - first_col = false - end - definition << ");\n\n" - end - definition - end - - def dump_schema_information - begin - if (current_schema = ActiveRecord::Migrator.current_version) > 0 - #TODO: Find a way to get the DB2 instace name to properly form the statement - return "INSERT INTO DB2INST2.SCHEMA_INFO (version) VALUES (#{current_schema})" - end - rescue ActiveRecord::StatementInvalid - # No Schema Info - end - end - - end -end diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-0.9.0.1/lib/jdbc_adapter/jdbc_hsqldb.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-0.9.0.1/lib/jdbc_adapter/jdbc_hsqldb.rb deleted file mode 100755 index 40e20c65b94..00000000000 --- a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-0.9.0.1/lib/jdbc_adapter/jdbc_hsqldb.rb +++ /dev/null @@ -1,200 +0,0 @@ -module ::JdbcSpec - module ActiveRecordExtensions - def hsqldb_connection(config) - config[:url] ||= "jdbc:hsqldb:#{config[:database]}" - config[:driver] ||= "org.hsqldb.jdbcDriver" - embedded_driver(config) - end - - def h2_connection(config) - config[:url] ||= "jdbc:h2:#{config[:database]}" - config[:driver] ||= "org.h2.Driver" - embedded_driver(config) - end - end - - module HSQLDB - def self.column_selector - [/hsqldb|\.h2\./i, lambda {|cfg,col| col.extend(::JdbcSpec::HSQLDB::Column)}] - end - - def self.adapter_selector - [/hsqldb|\.h2\./i, lambda do |cfg,adapt| - adapt.extend(::JdbcSpec::HSQLDB) - def adapt.h2_adapter; true; end if cfg[:driver] =~ /\.h2\./ - end] - end - - module Column - def type_cast(value) - return nil if value.nil? || value =~ /^\s*null\s*$/i - case type - when :string then value - when :integer then defined?(value.to_i) ? value.to_i : (value ? 1 : 0) - when :primary_key then defined?(value.to_i) ? value.to_i : (value ? 1 : 0) - when :float then value.to_f - when :datetime then cast_to_date_or_time(value) - when :timestamp then cast_to_time(value) - when :binary then value.scan(/[0-9A-Fa-f]{2}/).collect {|v| v.to_i(16)}.pack("C*") - when :time then cast_to_time(value) - else value - end - end - def cast_to_date_or_time(value) - return value if value.is_a? Date - return nil if value.blank? - guess_date_or_time((value.is_a? Time) ? value : cast_to_time(value)) - end - - def cast_to_time(value) - return value if value.is_a? Time - time_array = ParseDate.parsedate value - time_array[0] ||= 2000; time_array[1] ||= 1; time_array[2] ||= 1; - Time.send(ActiveRecord::Base.default_timezone, *time_array) rescue nil - end - - def guess_date_or_time(value) - (value.hour == 0 and value.min == 0 and value.sec == 0) ? - Date.new(value.year, value.month, value.day) : value - end - - - private - def simplified_type(field_type) - case field_type - when /longvarchar/i - :text - else - super(field_type) - end - end - - # Override of ActiveRecord::ConnectionAdapters::Column - def extract_limit(sql_type) - # HSQLDB appears to return "LONGVARCHAR(0)" for :text columns, which - # for AR purposes should be interpreted as "no limit" - return nil if sql_type =~ /\(0\)/ - super - end - end - - def modify_types(tp) - tp[:primary_key] = "INTEGER GENERATED BY DEFAULT AS IDENTITY(START WITH 0) PRIMARY KEY" - tp[:integer][:limit] = nil - tp[:boolean][:limit] = nil - # set text and float limits so we don't see odd scales tacked on - # in migrations - tp[:text][:limit] = nil - tp[:float][:limit] = 17 - tp[:string][:limit] = 255 - tp[:datetime] = { :name => "DATETIME" } - tp[:timestamp] = { :name => "DATETIME" } - tp[:time] = { :name => "DATETIME" } - tp[:date] = { :name => "DATETIME" } - tp - end - - def quote(value, column = nil) # :nodoc: - return value.quoted_id if value.respond_to?(:quoted_id) - - case value - when String - if respond_to?(:h2_adapter) && value.empty? - "NULL" - elsif column && column.type == :binary - "'#{quote_string(value).unpack("C*").collect {|v| v.to_s(16)}.join}'" - else - "'#{quote_string(value)}'" - end - else super - end - end - - def quote_string(str) - str.gsub(/'/, "''") - end - - def quoted_true - '1' - end - - def quoted_false - '0' - end - - def add_column(table_name, column_name, type, options = {}) - if option_not_null = options[:null] == false - option_not_null = options.delete(:null) - end - add_column_sql = "ALTER TABLE #{quote_table_name(table_name)} ADD #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}" - add_column_options!(add_column_sql, options) - execute(add_column_sql) - if option_not_null - alter_column_sql = "ALTER TABLE #{quote_table_name(table_name)} ALTER #{quote_column_name(column_name)} NOT NULL" - end - end - - def change_column(table_name, column_name, type, options = {}) #:nodoc: - execute "ALTER TABLE #{table_name} ALTER COLUMN #{column_name} #{type_to_sql(type, options[:limit])}" - end - - def change_column_default(table_name, column_name, default) #:nodoc: - execute "ALTER TABLE #{table_name} ALTER COLUMN #{column_name} SET DEFAULT #{quote(default)}" - end - - def rename_column(table_name, column_name, new_column_name) #:nodoc: - execute "ALTER TABLE #{table_name} ALTER COLUMN #{column_name} RENAME TO #{new_column_name}" - end - - def rename_table(name, new_name) - execute "ALTER TABLE #{name} RENAME TO #{new_name}" - end - - def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) #:nodoc: - log(sql,name) do - @connection.execute_update(sql) - end - table = sql.split(" ", 4)[2] - id_value || last_insert_id(table, nil) - end - - def last_insert_id(table, sequence_name) - Integer(select_value("SELECT IDENTITY() FROM #{table}")) - end - - # Override normal #_execute: See Rubyforge #11567 - def _execute(sql, name = nil) - if ::ActiveRecord::ConnectionAdapters::JdbcConnection::select?(sql) - @connection.execute_query(sql) - elsif ::ActiveRecord::ConnectionAdapters::JdbcConnection::insert?(sql) - insert(sql, name) - else - @connection.execute_update(sql) - end - end - - def add_limit_offset!(sql, options) #:nodoc: - offset = options[:offset] || 0 - bef = sql[7..-1] - if limit = options[:limit] - sql.replace "select limit #{offset} #{limit} #{bef}" - elsif offset > 0 - sql.replace "select limit #{offset} 0 #{bef}" - end - end - - # override to filter out system tables that otherwise end - # up in db/schema.rb during migrations. JdbcConnection#tables - # now takes an optional block filter so we can screen out - # rows corresponding to system tables. HSQLDB names its - # system tables SYSTEM.*, but H2 seems to name them without - # any kind of convention - def tables - @connection.tables.select {|row| row.to_s !~ /^system_/i } - end - - def remove_index(table_name, options = {}) - execute "DROP INDEX #{quote_column_name(index_name(table_name, options))}" - end - end -end diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-0.9.0.1/lib/jdbc_adapter/jdbc_mssql.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-0.9.0.1/lib/jdbc_adapter/jdbc_mssql.rb deleted file mode 100755 index 4b116a17889..00000000000 --- a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-0.9.0.1/lib/jdbc_adapter/jdbc_mssql.rb +++ /dev/null @@ -1,315 +0,0 @@ -require 'jdbc_adapter/tsql_helper' - -module ::ActiveRecord - class Base - # After setting large objects to empty, write data back with a helper method - after_save :write_lobs - def write_lobs() #:nodoc: - if connection.is_a?(JdbcSpec::MsSQL) - self.class.columns.select { |c| c.sql_type =~ /image/i }.each { |c| - value = self[c.name] - value = value.to_yaml if unserializable_attribute?(c.name, c) - next if value.nil? || (value == '') - - connection.write_large_object(c.type == :binary, c.name, self.class.table_name, self.class.primary_key, quote_value(id), value) - } - end - end - private :write_lobs - end -end - -module JdbcSpec - module MsSQL - include TSqlMethods - - def self.column_selector - [/sqlserver|tds/i, lambda {|cfg,col| col.extend(::JdbcSpec::MsSQL::Column)}] - end - - def self.adapter_selector - [/sqlserver|tds/i, lambda {|cfg,adapt| adapt.extend(::JdbcSpec::MsSQL)}] - end - - module Column - attr_accessor :identity, :is_special - - def simplified_type(field_type) - case field_type - when /int|bigint|smallint|tinyint/i then :integer - when /numeric/i then (@scale.nil? || @scale == 0) ? :integer : :decimal - when /float|double|decimal|money|real|smallmoney/i then :decimal - when /datetime|smalldatetime/i then :datetime - when /timestamp/i then :timestamp - when /time/i then :time - when /text|ntext/i then :text - when /binary|image|varbinary/i then :binary - when /char|nchar|nvarchar|string|varchar/i then :string - when /bit/i then :boolean - when /uniqueidentifier/i then :string - end - end - - def type_cast(value) - return nil if value.nil? || value == "(null)" || value == "(NULL)" - case type - when :string then unquote_string value - when :integer then unquote(value).to_i rescue value ? 1 : 0 - when :primary_key then value == true || value == false ? value == true ? 1 : 0 : value.to_i - when :decimal then self.class.value_to_decimal(unquote(value)) - when :datetime then cast_to_datetime(value) - when :timestamp then cast_to_time(value) - when :time then cast_to_time(value) - when :date then cast_to_datetime(value) - when :boolean then value == true or (value =~ /^t(rue)?$/i) == 0 or unquote(value)=="1" - when :binary then unquote value - else value - end - end - - # JRUBY-2011: Match balanced quotes and parenthesis - 'text',('text') or (text) - def unquote_string(value) - value.sub(/^\((.*)\)$/,'\1').sub(/^'(.*)'$/,'\1') - end - - def unquote(value) - value.to_s.sub(/\A\([\(\']?/, "").sub(/[\'\)]?\)\Z/, "") - end - - def cast_to_time(value) - return value if value.is_a?(Time) - time_array = ParseDate.parsedate(value) - time_array[0] ||= 2000 - time_array[1] ||= 1 - time_array[2] ||= 1 - Time.send(ActiveRecord::Base.default_timezone, *time_array) rescue nil - end - - def cast_to_datetime(value) - if value.is_a?(Time) - if value.year != 0 and value.month != 0 and value.day != 0 - return value - else - return Time.mktime(2000, 1, 1, value.hour, value.min, value.sec) rescue nil - end - end - return cast_to_time(value) if value.is_a?(Date) or value.is_a?(String) rescue nil - value - end - - # These methods will only allow the adapter to insert binary data with a length of 7K or less - # because of a SQL Server statement length policy. - def self.string_to_binary(value) - '' - end - end - - def quote(value, column = nil) - return value.quoted_id if value.respond_to?(:quoted_id) - - case value - when String, ActiveSupport::Multibyte::Chars - value = value.to_s - if column && column.type == :binary - "'#{quote_string(JdbcSpec::MsSQL::Column.string_to_binary(value))}'" # ' (for ruby-mode) - elsif column && [:integer, :float].include?(column.type) - value = column.type == :integer ? value.to_i : value.to_f - value.to_s - else - "'#{quote_string(value)}'" # ' (for ruby-mode) - end - when TrueClass then '1' - when FalseClass then '0' - when Time, DateTime then "'#{value.strftime("%Y%m%d %H:%M:%S")}'" - when Date then "'#{value.strftime("%Y%m%d")}'" - else super - end - end - - def quote_string(string) - string.gsub(/\'/, "''") - end - - def quote_table_name(name) - name - end - - def quote_column_name(name) - "[#{name}]" - end - - def change_order_direction(order) - order.split(",").collect {|fragment| - case fragment - when /\bDESC\b/i then fragment.gsub(/\bDESC\b/i, "ASC") - when /\bASC\b/i then fragment.gsub(/\bASC\b/i, "DESC") - else String.new(fragment).split(',').join(' DESC,') + ' DESC' - end - }.join(",") - end - - def recreate_database(name) - drop_database(name) - create_database(name) - end - - def drop_database(name) - execute "DROP DATABASE #{name}" - end - - def create_database(name) - execute "CREATE DATABASE #{name}" - end - - def rename_table(name, new_name) - execute "EXEC sp_rename '#{name}', '#{new_name}'" - end - - # Adds a new column to the named table. - # See TableDefinition#column for details of the options you can use. - def add_column(table_name, column_name, type, options = {}) - add_column_sql = "ALTER TABLE #{table_name} ADD #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}" - add_column_options!(add_column_sql, options) - # TODO: Add support to mimic date columns, using constraints to mark them as such in the database - # add_column_sql << " CONSTRAINT ck__#{table_name}__#{column_name}__date_only CHECK ( CONVERT(CHAR(12), #{quote_column_name(column_name)}, 14)='00:00:00:000' )" if type == :date - execute(add_column_sql) - end - - def rename_column(table, column, new_column_name) - execute "EXEC sp_rename '#{table}.#{column}', '#{new_column_name}'" - end - - def change_column(table_name, column_name, type, options = {}) #:nodoc: - sql_commands = ["ALTER TABLE #{table_name} ALTER COLUMN #{column_name} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"] - if options_include_default?(options) - remove_default_constraint(table_name, column_name) - sql_commands << "ALTER TABLE #{table_name} ADD CONSTRAINT DF_#{table_name}_#{column_name} DEFAULT #{quote(options[:default], options[:column])} FOR #{column_name}" - end - sql_commands.each {|c| - execute(c) - } - end - def change_column_default(table_name, column_name, default) #:nodoc: - remove_default_constraint(table_name, column_name) - execute "ALTER TABLE #{table_name} ADD CONSTRAINT DF_#{table_name}_#{column_name} DEFAULT #{quote(default, column_name)} FOR #{column_name}" - end - def remove_column(table_name, column_name) - remove_check_constraints(table_name, column_name) - remove_default_constraint(table_name, column_name) - execute "ALTER TABLE #{table_name} DROP COLUMN [#{column_name}]" - end - - def remove_default_constraint(table_name, column_name) - defaults = select "select def.name from sysobjects def, syscolumns col, sysobjects tab where col.cdefault = def.id and col.name = '#{column_name}' and tab.name = '#{table_name}' and col.id = tab.id" - defaults.each {|constraint| - execute "ALTER TABLE #{table_name} DROP CONSTRAINT #{constraint["name"]}" - } - end - - def remove_check_constraints(table_name, column_name) - # TODO remove all constraints in single method - constraints = select "SELECT CONSTRAINT_NAME FROM INFORMATION_SCHEMA.CONSTRAINT_COLUMN_USAGE where TABLE_NAME = '#{table_name}' and COLUMN_NAME = '#{column_name}'" - constraints.each do |constraint| - execute "ALTER TABLE #{table_name} DROP CONSTRAINT #{constraint["CONSTRAINT_NAME"]}" - end - end - - def remove_index(table_name, options = {}) - execute "DROP INDEX #{table_name}.#{index_name(table_name, options)}" - end - - - def columns(table_name, name = nil) - return [] if table_name =~ /^information_schema\./i - cc = super - cc.each do |col| - col.identity = true if col.sql_type =~ /identity/i - col.is_special = true if col.sql_type =~ /text|ntext|image/i - end - cc - end - - def _execute(sql, name = nil) - if sql.lstrip =~ /^insert/i - if query_requires_identity_insert?(sql) - table_name = get_table_name(sql) - with_identity_insert_enabled(table_name) do - id = @connection.execute_insert(sql) - end - else - @connection.execute_insert(sql) - end - elsif sql.lstrip =~ /^\(?\s*(select|show)/i - repair_special_columns(sql) - @connection.execute_query(sql) - else - @connection.execute_update(sql) - end - end - - - private - # Turns IDENTITY_INSERT ON for table during execution of the block - # N.B. This sets the state of IDENTITY_INSERT to OFF after the - # block has been executed without regard to its previous state - - def with_identity_insert_enabled(table_name, &block) - set_identity_insert(table_name, true) - yield - ensure - set_identity_insert(table_name, false) - end - - def set_identity_insert(table_name, enable = true) - execute "SET IDENTITY_INSERT #{table_name} #{enable ? 'ON' : 'OFF'}" - rescue Exception => e - raise ActiveRecord::ActiveRecordError, "IDENTITY_INSERT could not be turned #{enable ? 'ON' : 'OFF'} for table #{table_name}" - end - - def get_table_name(sql) - if sql =~ /^\s*insert\s+into\s+([^\(\s,]+)\s*|^\s*update\s+([^\(\s,]+)\s*/i - $1 - elsif sql =~ /from\s+([^\(\s,]+)\s*/i - $1 - else - nil - end - end - - def identity_column(table_name) - @table_columns = {} unless @table_columns - @table_columns[table_name] = columns(table_name) if @table_columns[table_name] == nil - @table_columns[table_name].each do |col| - return col.name if col.identity - end - - return nil - end - - def query_requires_identity_insert?(sql) - table_name = get_table_name(sql) - id_column = identity_column(table_name) - sql =~ /\[#{id_column}\]/ ? table_name : nil - end - - def get_special_columns(table_name) - special = [] - @table_columns ||= {} - @table_columns[table_name] ||= columns(table_name) - @table_columns[table_name].each do |col| - special << col.name if col.is_special - end - special - end - - def repair_special_columns(sql) - special_cols = get_special_columns(get_table_name(sql)) - for col in special_cols.to_a - sql.gsub!(Regexp.new(" #{col.to_s} = "), " #{col.to_s} LIKE ") - sql.gsub!(/ORDER BY #{col.to_s}/i, '') - end - sql - end - end - end - diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-0.9.0.1/lib/jdbc_adapter/jdbc_mysql.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-0.9.0.1/lib/jdbc_adapter/jdbc_mysql.rb deleted file mode 100755 index 9db891c4482..00000000000 --- a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-0.9.0.1/lib/jdbc_adapter/jdbc_mysql.rb +++ /dev/null @@ -1,235 +0,0 @@ -require 'active_record/connection_adapters/abstract/schema_definitions' - -module ::JdbcSpec - # Don't need to load native mysql adapter - $LOADED_FEATURES << "active_record/connection_adapters/mysql_adapter.rb" - - module ActiveRecordExtensions - def mysql_connection(config) - config[:port] ||= 3306 - if config[:url] - config[:url] = config[:url]['?'] ? "#{config[:url]}&#{MySQL::URL_OPTIONS}" : "#{config[:url]}?#{MySQL::URL_OPTIONS}" - else - config[:url] = "jdbc:mysql://#{config[:host]}:#{config[:port]}/#{config[:database]}?#{MySQL::URL_OPTIONS}" - end - config[:driver] = "com.mysql.jdbc.Driver" - jdbc_connection(config) - end - end - - module MySQL - URL_OPTIONS = "zeroDateTimeBehavior=convertToNull&jdbcCompliantTruncation=false&useUnicode=true&characterEncoding=utf8" - def self.column_selector - [/mysql/i, lambda {|cfg,col| col.extend(::JdbcSpec::MySQL::Column)}] - end - - def self.adapter_selector - [/mysql/i, lambda {|cfg,adapt| adapt.extend(::JdbcSpec::MySQL)}] - end - - def self.extended(adapter) - adapter.execute("SET SQL_AUTO_IS_NULL=0") - end - - module Column - TYPES_ALLOWING_EMPTY_STRING_DEFAULT = Set.new([:binary, :string, :text]) - - def simplified_type(field_type) - return :boolean if field_type =~ /tinyint\(1\)|bit/i - return :string if field_type =~ /enum/i - super - end - - def init_column(name, default, *args) - @original_default = default - @default = nil if missing_default_forged_as_empty_string? - end - - # MySQL misreports NOT NULL column default when none is given. - # We can't detect this for columns which may have a legitimate '' - # default (string, text, binary) but we can for others (integer, - # datetime, boolean, and the rest). - # - # Test whether the column has default '', is not null, and is not - # a type allowing default ''. - def missing_default_forged_as_empty_string? - !null && @original_default == '' && !TYPES_ALLOWING_EMPTY_STRING_DEFAULT.include?(type) - end - end - - def modify_types(tp) - tp[:primary_key] = "int(11) DEFAULT NULL auto_increment PRIMARY KEY" - tp[:decimal] = { :name => "decimal" } - tp[:timestamp] = { :name => "datetime" } - tp[:datetime][:limit] = nil - tp - end - - # QUOTING ================================================== - - def quote(value, column = nil) - return value.quoted_id if value.respond_to?(:quoted_id) - - if column && column.type == :primary_key - value.to_s - elsif column && String === value && column.type == :binary && column.class.respond_to?(:string_to_binary) - s = column.class.string_to_binary(value).unpack("H*")[0] - "x'#{s}'" - elsif BigDecimal === value - "'#{value.to_s("F")}'" - else - super - end - end - - def quote_column_name(name) #:nodoc: - "`#{name}`" - end - - def quote_table_name(name) #:nodoc: - quote_column_name(name).gsub('.', '`.`') - end - - def quoted_true - "1" - end - - def quoted_false - "0" - end - - def begin_db_transaction #:nodoc: - @connection.begin - rescue Exception - # Transactions aren't supported - end - - def commit_db_transaction #:nodoc: - @connection.commit - rescue Exception - # Transactions aren't supported - end - - def rollback_db_transaction #:nodoc: - @connection.rollback - rescue Exception - # Transactions aren't supported - end - - def disable_referential_integrity(&block) #:nodoc: - old = select_value("SELECT @@FOREIGN_KEY_CHECKS") - begin - update("SET FOREIGN_KEY_CHECKS = 0") - yield - ensure - update("SET FOREIGN_KEY_CHECKS = #{old}") - end - end - - # SCHEMA STATEMENTS ======================================== - - def structure_dump #:nodoc: - if supports_views? - sql = "SHOW FULL TABLES WHERE Table_type = 'BASE TABLE'" - else - sql = "SHOW TABLES" - end - - select_all(sql).inject("") do |structure, table| - table.delete('Table_type') - - hash = select_one("SHOW CREATE TABLE #{quote_table_name(table.to_a.first.last)}") - - if(table = hash["Create Table"]) - structure += table + ";\n\n" - elsif(view = hash["Create View"]) - structure += view + ";\n\n" - end - end - end - - def recreate_database(name) #:nodoc: - drop_database(name) - create_database(name) - end - - def create_database(name, options = {}) #:nodoc: - if options[:collation] - execute "CREATE DATABASE `#{name}` DEFAULT CHARACTER SET `#{options[:charset] || 'utf8'}` COLLATE `#{options[:collation]}`" - else - execute "CREATE DATABASE `#{name}` DEFAULT CHARACTER SET `#{options[:charset] || 'utf8'}`" - end - end - - def drop_database(name) #:nodoc: - execute "DROP DATABASE IF EXISTS `#{name}`" - end - - def current_database - select_one("SELECT DATABASE() as db")["db"] - end - - def create_table(name, options = {}) #:nodoc: - super(name, {:options => "ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin"}.merge(options)) - end - - def rename_table(name, new_name) - execute "RENAME TABLE #{quote_table_name(name)} TO #{quote_table_name(new_name)}" - end - - def change_column_default(table_name, column_name, default) #:nodoc: - current_type = select_one("SHOW COLUMNS FROM #{quote_table_name(table_name)} LIKE '#{column_name}'")["Type"] - - execute("ALTER TABLE #{quote_table_name(table_name)} CHANGE #{quote_column_name(column_name)} #{quote_column_name(column_name)} #{current_type} DEFAULT #{quote(default)}") - end - - def change_column(table_name, column_name, type, options = {}) #:nodoc: - unless options_include_default?(options) - if column = columns(table_name).find { |c| c.name == column_name.to_s } - options[:default] = column.default - else - raise "No such column: #{table_name}.#{column_name}" - end - end - - change_column_sql = "ALTER TABLE #{quote_table_name(table_name)} CHANGE #{quote_column_name(column_name)} #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}" - add_column_options!(change_column_sql, options) - execute(change_column_sql) - end - - def rename_column(table_name, column_name, new_column_name) #:nodoc: - cols = select_one("SHOW COLUMNS FROM #{quote_table_name(table_name)} LIKE '#{column_name}'") - current_type = cols["Type"] || cols["COLUMN_TYPE"] - execute "ALTER TABLE #{quote_table_name(table_name)} CHANGE #{quote_table_name(column_name)} #{quote_column_name(new_column_name)} #{current_type}" - end - - def add_limit_offset!(sql, options) #:nodoc: - if limit = options[:limit] - unless offset = options[:offset] - sql << " LIMIT #{limit}" - else - sql << " LIMIT #{offset}, #{limit}" - end - end - end - - def show_variable(var) - res = execute("show variables like '#{var}'") - row = res.detect {|row| row["Variable_name"] == var } - row && row["Value"] - end - - def charset - show_variable("character_set_database") - end - - def collation - show_variable("collation_database") - end - - private - def supports_views? - false - end - end -end diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-0.9.0.1/lib/jdbc_adapter/jdbc_postgre.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-0.9.0.1/lib/jdbc_adapter/jdbc_postgre.rb deleted file mode 100755 index 2cbb54576a3..00000000000 --- a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-0.9.0.1/lib/jdbc_adapter/jdbc_postgre.rb +++ /dev/null @@ -1,403 +0,0 @@ -module ::JdbcSpec - # Don't need to load native postgres adapter - $LOADED_FEATURES << "active_record/connection_adapters/postgresql_adapter.rb" - - module ActiveRecordExtensions - def postgresql_connection(config) - config[:host] ||= "localhost" - config[:port] ||= 5432 - config[:url] ||= "jdbc:postgresql://#{config[:host]}:#{config[:port]}/#{config[:database]}" - config[:url] << config[:pg_params] if config[:pg_params] - config[:driver] ||= "org.postgresql.Driver" - jdbc_connection(config) - end - end - - module PostgreSQL - def self.column_selector - [/postgre/i, lambda {|cfg,col| col.extend(::JdbcSpec::PostgreSQL::Column)}] - end - - def self.adapter_selector - [/postgre/i, lambda {|cfg,adapt| adapt.extend(::JdbcSpec::PostgreSQL)}] - end - - module Column - def type_cast(value) - case type - when :boolean then cast_to_boolean(value) - else super - end - end - - def simplified_type(field_type) - return :integer if field_type =~ /^serial/i - return :string if field_type =~ /\[\]$/i || field_type =~ /^interval/i - return :string if field_type =~ /^(?:point|lseg|box|"?path"?|polygon|circle)/i - return :datetime if field_type =~ /^timestamp/i - return :float if field_type =~ /^real|^money/i - return :binary if field_type =~ /^bytea/i - return :boolean if field_type =~ /^bool/i - super - end - - def cast_to_boolean(value) - if value == true || value == false - value - else - %w(true t 1).include?(value.to_s.downcase) - end - end - - def cast_to_date_or_time(value) - return value if value.is_a? Date - return nil if value.blank? - guess_date_or_time((value.is_a? Time) ? value : cast_to_time(value)) - end - - def cast_to_time(value) - return value if value.is_a? Time - time_array = ParseDate.parsedate value - time_array[0] ||= 2000; time_array[1] ||= 1; time_array[2] ||= 1; - Time.send(ActiveRecord::Base.default_timezone, *time_array) rescue nil - end - - def guess_date_or_time(value) - (value.hour == 0 and value.min == 0 and value.sec == 0) ? - Date.new(value.year, value.month, value.day) : value - end - - def default_value(value) - # Boolean types - return "t" if value =~ /true/i - return "f" if value =~ /false/i - - # Char/String/Bytea type values - return $1 if value =~ /^'(.*)'::(bpchar|text|character varying|bytea)$/ - - # Numeric values - return value if value =~ /^-?[0-9]+(\.[0-9]*)?/ - - # Fixed dates / timestamp - return $1 if value =~ /^'(.+)'::(date|timestamp)/ - - # Anything else is blank, some user type, or some function - # and we can't know the value of that, so return nil. - return nil - end - end - - def modify_types(tp) - tp[:primary_key] = "serial primary key" - tp[:string][:limit] = 255 - tp[:integer][:limit] = nil - tp[:boolean][:limit] = nil - tp - end - - def default_sequence_name(table_name, pk = nil) - default_pk, default_seq = pk_and_sequence_for(table_name) - default_seq || "#{table_name}_#{pk || default_pk || 'id'}_seq" - end - - # Resets sequence to the max value of the table's pk if present. - def reset_pk_sequence!(table, pk = nil, sequence = nil) - unless pk and sequence - default_pk, default_sequence = pk_and_sequence_for(table) - pk ||= default_pk - sequence ||= default_sequence - end - if pk - if sequence - select_value <<-end_sql, 'Reset sequence' - SELECT setval('#{sequence}', (SELECT COALESCE(MAX(#{pk})+(SELECT increment_by FROM #{sequence}), (SELECT min_value FROM #{sequence})) FROM #{table}), false) - end_sql - else - @logger.warn "#{table} has primary key #{pk} with no default sequence" if @logger - end - end - end - - # Find a table's primary key and sequence. - def pk_and_sequence_for(table) - # First try looking for a sequence with a dependency on the - # given table's primary key. - result = select(<<-end_sql, 'PK and serial sequence')[0] - SELECT attr.attname AS nm, name.nspname AS nsp, seq.relname AS rel - FROM pg_class seq, - pg_attribute attr, - pg_depend dep, - pg_namespace name, - pg_constraint cons - WHERE seq.oid = dep.objid - AND seq.relnamespace = name.oid - AND seq.relkind = 'S' - AND attr.attrelid = dep.refobjid - AND attr.attnum = dep.refobjsubid - AND attr.attrelid = cons.conrelid - AND attr.attnum = cons.conkey[1] - AND cons.contype = 'p' - AND dep.refobjid = '#{table}'::regclass - end_sql - - if result.nil? or result.empty? - # If that fails, try parsing the primary key's default value. - # Support the 7.x and 8.0 nextval('foo'::text) as well as - # the 8.1+ nextval('foo'::regclass). - # TODO: assumes sequence is in same schema as table. - result = select(<<-end_sql, 'PK and custom sequence')[0] - SELECT attr.attname AS nm, name.nspname AS nsp, split_part(def.adsrc, '\\\'', 2) AS rel - FROM pg_class t - JOIN pg_namespace name ON (t.relnamespace = name.oid) - JOIN pg_attribute attr ON (t.oid = attrelid) - JOIN pg_attrdef def ON (adrelid = attrelid AND adnum = attnum) - JOIN pg_constraint cons ON (conrelid = adrelid AND adnum = conkey[1]) - WHERE t.oid = '#{table}'::regclass - AND cons.contype = 'p' - AND def.adsrc ~* 'nextval' - end_sql - end - # check for existence of . in sequence name as in public.foo_sequence. if it does not exist, join the current namespace - result['rel']['.'] ? [result['nm'], result['rel']] : [result['nm'], "#{result['nsp']}.#{result['rel']}"] - rescue - nil - end - - def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) #:nodoc: - execute(sql, name) - table = sql.split(" ", 4)[2] - id_value || pk && last_insert_id(table, sequence_name || default_sequence_name(table, pk)) - end - - def columns(table_name, name=nil) - schema_name = "public" - if table_name =~ /\./ - parts = table_name.split(/\./) - table_name = parts.pop - schema_name = parts.join(".") - end - @connection.columns_internal(table_name, name, schema_name) - end - - # From postgresql_adapter.rb - def indexes(table_name, name = nil) - result = select_rows(<<-SQL, name) - SELECT i.relname, d.indisunique, a.attname - FROM pg_class t, pg_class i, pg_index d, pg_attribute a - WHERE i.relkind = 'i' - AND d.indexrelid = i.oid - AND d.indisprimary = 'f' - AND t.oid = d.indrelid - AND t.relname = '#{table_name}' - AND a.attrelid = t.oid - AND ( d.indkey[0]=a.attnum OR d.indkey[1]=a.attnum - OR d.indkey[2]=a.attnum OR d.indkey[3]=a.attnum - OR d.indkey[4]=a.attnum OR d.indkey[5]=a.attnum - OR d.indkey[6]=a.attnum OR d.indkey[7]=a.attnum - OR d.indkey[8]=a.attnum OR d.indkey[9]=a.attnum ) - ORDER BY i.relname - SQL - - current_index = nil - indexes = [] - - result.each do |row| - if current_index != row[0] - indexes << ::ActiveRecord::ConnectionAdapters::IndexDefinition.new(table_name, row[0], row[1] == "t", []) - current_index = row[0] - end - - indexes.last.columns << row[2] - end - - indexes - end - - def last_insert_id(table, sequence_name) - Integer(select_value("SELECT currval('#{sequence_name}')")) - end - - def recreate_database(name) - drop_database(name) - create_database(name) - end - - def create_database(name, options = {}) - execute "CREATE DATABASE \"#{name}\" ENCODING='#{options[:encoding] || 'utf8'}'" - end - - def drop_database(name) - execute "DROP DATABASE \"#{name}\"" - end - - def structure_dump - database = @config[:database] - if database.nil? - if @config[:url] =~ /\/([^\/]*)$/ - database = $1 - else - raise "Could not figure out what database this url is for #{@config["url"]}" - end - end - - ENV['PGHOST'] = @config[:host] if @config[:host] - ENV['PGPORT'] = @config[:port].to_s if @config[:port] - ENV['PGPASSWORD'] = @config[:password].to_s if @config[:password] - search_path = @config[:schema_search_path] - search_path = "--schema=#{search_path}" if search_path - - @connection.connection.close - begin - file = "db/#{RAILS_ENV}_structure.sql" - `pg_dump -i -U "#{@config[:username]}" -s -x -O -f #{file} #{search_path} #{database}` - raise "Error dumping database" if $?.exitstatus == 1 - - # need to patch away any references to SQL_ASCII as it breaks the JDBC driver - lines = File.readlines(file) - File.open(file, "w") do |io| - lines.each do |line| - line.gsub!(/SQL_ASCII/, 'UNICODE') - io.write(line) - end - end - ensure - reconnect! - end - end - - def _execute(sql, name = nil) - case sql.strip - when /\A\(?\s*(select|show)/i: - @connection.execute_query(sql) - else - @connection.execute_update(sql) - end - end - - # SELECT DISTINCT clause for a given set of columns and a given ORDER BY clause. - # - # PostgreSQL requires the ORDER BY columns in the select list for distinct queries, and - # requires that the ORDER BY include the distinct column. - # - # distinct("posts.id", "posts.created_at desc") - def distinct(columns, order_by) - return "DISTINCT #{columns}" if order_by.blank? - - # construct a clean list of column names from the ORDER BY clause, removing - # any asc/desc modifiers - order_columns = order_by.split(',').collect { |s| s.split.first } - order_columns.delete_if(&:blank?) - order_columns = order_columns.zip((0...order_columns.size).to_a).map { |s,i| "#{s} AS alias_#{i}" } - - # return a DISTINCT ON() clause that's distinct on the columns we want but includes - # all the required columns for the ORDER BY to work properly - sql = "DISTINCT ON (#{columns}) #{columns}, " - sql << order_columns * ', ' - end - - # ORDER BY clause for the passed order option. - # - # PostgreSQL does not allow arbitrary ordering when using DISTINCT ON, so we work around this - # by wrapping the sql as a sub-select and ordering in that query. - def add_order_by_for_association_limiting!(sql, options) - return sql if options[:order].blank? - - order = options[:order].split(',').collect { |s| s.strip }.reject(&:blank?) - order.map! { |s| 'DESC' if s =~ /\bdesc$/i } - order = order.zip((0...order.size).to_a).map { |s,i| "id_list.alias_#{i} #{s}" }.join(', ') - - sql.replace "SELECT * FROM (#{sql}) AS id_list ORDER BY #{order}" - end - - def quote(value, column = nil) - return value.quoted_id if value.respond_to?(:quoted_id) - - if value.kind_of?(String) && column && column.type == :binary - "'#{escape_bytea(value)}'" - elsif column && column.type == :primary_key - return value.to_s - else - super - end - end - - def escape_bytea(s) - if s - result = '' - s.each_byte { |c| result << sprintf('\\\\%03o', c) } - result - end - end - - def quote_column_name(name) - %("#{name}") - end - - def quoted_date(value) - value.strftime("%Y-%m-%d %H:%M:%S") - end - - def disable_referential_integrity(&block) #:nodoc: - execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} DISABLE TRIGGER ALL" }.join(";")) - yield - ensure - execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} ENABLE TRIGGER ALL" }.join(";")) - end - - def rename_table(name, new_name) - execute "ALTER TABLE #{name} RENAME TO #{new_name}" - end - - def add_column(table_name, column_name, type, options = {}) - execute("ALTER TABLE #{table_name} ADD #{column_name} #{type_to_sql(type, options[:limit])}") - change_column_default(table_name, column_name, options[:default]) unless options[:default].nil? - if options[:null] == false - execute("UPDATE #{table_name} SET #{column_name} = '#{options[:default]}'") if options[:default] - execute("ALTER TABLE #{table_name} ALTER #{column_name} SET NOT NULL") - end - end - - def change_column(table_name, column_name, type, options = {}) #:nodoc: - begin - execute "ALTER TABLE #{table_name} ALTER #{column_name} TYPE #{type_to_sql(type, options[:limit])}" - rescue ActiveRecord::StatementInvalid - # This is PG7, so we use a more arcane way of doing it. - begin_db_transaction - add_column(table_name, "#{column_name}_ar_tmp", type, options) - execute "UPDATE #{table_name} SET #{column_name}_ar_tmp = CAST(#{column_name} AS #{type_to_sql(type, options[:limit])})" - remove_column(table_name, column_name) - rename_column(table_name, "#{column_name}_ar_tmp", column_name) - commit_db_transaction - end - change_column_default(table_name, column_name, options[:default]) unless options[:default].nil? - end - - def change_column_default(table_name, column_name, default) #:nodoc: - execute "ALTER TABLE #{table_name} ALTER COLUMN #{column_name} SET DEFAULT '#{default}'" - end - - def rename_column(table_name, column_name, new_column_name) #:nodoc: - execute "ALTER TABLE #{table_name} RENAME COLUMN #{column_name} TO #{new_column_name}" - end - - def remove_index(table_name, options) #:nodoc: - execute "DROP INDEX #{index_name(table_name, options)}" - end - - def type_to_sql(type, limit = nil, precision = nil, scale = nil) #:nodoc: - return super unless type.to_s == 'integer' - - if limit.nil? || limit == 4 - 'integer' - elsif limit < 4 - 'smallint' - else - 'bigint' - end - end - - def tables - @connection.tables(database_name, nil, nil, ["TABLE"]) - end - end -end diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-0.9.0.1/lib/jdbc_adapter/jdbc_sqlite3.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-0.9.0.1/lib/jdbc_adapter/jdbc_sqlite3.rb deleted file mode 100755 index 3ddf7558917..00000000000 --- a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-0.9.0.1/lib/jdbc_adapter/jdbc_sqlite3.rb +++ /dev/null @@ -1,188 +0,0 @@ -module ::JdbcSpec - module ActiveRecordExtensions - def sqlite3_connection(config) - config[:url] ||= "jdbc:sqlite:#{config[:database]}" - config[:driver] ||= "org.sqlite.JDBC" - jdbc_connection(config) - end - end - - module SQLite3 - def self.column_selector - [/sqlite/i, lambda {|cfg,col| col.extend(::JdbcSpec::SQLite3::Column)}] - end - - def self.adapter_selector - [/sqlite/i, lambda {|cfg,adapt| adapt.extend(::JdbcSpec::SQLite3)}] - end - - module Column - - private - def simplified_type(field_type) - case field_type - when /^integer\(1\)$/i then :boolean - when /text/i then :string - when /int/i then :integer - when /real/i then @scale == 0 ? :integer : :decimal - when /date|time/i then :datetime - when /blob/i then :binary - end - end - - def self.cast_to_date_or_time(value) - return value if value.is_a? Date - return nil if value.blank? - guess_date_or_time((value.is_a? Time) ? value : cast_to_time(value)) - end - - def self.cast_to_time(value) - return value if value.is_a? Time - Time.at(value) rescue nil - end - - def self.guess_date_or_time(value) - (value.hour == 0 and value.min == 0 and value.sec == 0) ? - Date.new(value.year, value.month, value.day) : value - end - end - - def type_cast(value) - return nil if value.nil? - case type - when :string then value - when :integer then defined?(value.to_i) ? value.to_i : (value ? 1 : 0) - when :primary_key then defined?(value.to_i) ? value.to_i : (value ? 1 : 0) - when :float then value.to_f - when :datetime then JdbcSpec::SQLite3::Column.cast_to_date_or_time(value) - when :time then JdbcSpec::SQLite3::Column.cast_to_time(value) - when :decimal then self.class.value_to_decimal(value) - when :boolean then self.class.value_to_boolean(value) - else value - end - end - - def modify_types(tp) - tp[:primary_key] = "INTEGER PRIMARY KEY AUTOINCREMENT" - tp[:float] = { :name => "REAL" } - tp[:decimal] = { :name => "REAL" } - tp[:datetime] = { :name => "INTEGER" } - tp[:timestamp] = { :name => "INTEGER" } - tp[:time] = { :name => "INTEGER" } - tp[:date] = { :name => "INTEGER" } - tp[:boolean] = { :name => "INTEGER", :limit => 1} - tp - end - - def quote(value, column = nil) # :nodoc: - return value.quoted_id if value.respond_to?(:quoted_id) - - case value - when String - if column && column.type == :binary - "'#{quote_string(value).unpack("C*").collect {|v| v.to_s(16)}.join}'" - else - "'#{quote_string(value)}'" - end - else super - end - end - - def quote_string(str) - str.gsub(/'/, "''") - end - - def quoted_true - '1' - end - - def quoted_false - '0' - end - - def add_column(table_name, column_name, type, options = {}) - if option_not_null = options[:null] == false - option_not_null = options.delete(:null) - end - add_column_sql = "ALTER TABLE #{quote_table_name(table_name)} ADD #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}" - add_column_options!(add_column_sql, options) - execute(add_column_sql) - if option_not_null - alter_column_sql = "ALTER TABLE #{quote_table_name(table_name)} ALTER #{quote_column_name(column_name)} NOT NULL" - end - end - - def remove_column(table_name, column_name) #:nodoc: - cols = columns(table_name).collect {|col| col.name} - cols.delete(column_name) - cols = cols.join(', ') - table_backup = table_name + "_backup" - - @connection.begin - - execute "CREATE TEMPORARY TABLE #{table_backup}(#{cols})" - insert "INSERT INTO #{table_backup} SELECT #{cols} FROM #{table_name}" - execute "DROP TABLE #{table_name}" - execute "CREATE TABLE #{table_name}(#{cols})" - insert "INSERT INTO #{table_name} SELECT #{cols} FROM #{table_backup}" - execute "DROP TABLE #{table_backup}" - - @connection.commit - end - - def change_column(table_name, column_name, type, options = {}) #:nodoc: - execute "ALTER TABLE #{table_name} ALTER COLUMN #{column_name} #{type_to_sql(type, options[:limit])}" - end - - def change_column_default(table_name, column_name, default) #:nodoc: - execute "ALTER TABLE #{table_name} ALTER COLUMN #{column_name} SET DEFAULT #{quote(default)}" - end - - def rename_column(table_name, column_name, new_column_name) #:nodoc: - execute "ALTER TABLE #{table_name} ALTER COLUMN #{column_name} RENAME TO #{new_column_name}" - end - - def rename_table(name, new_name) - execute "ALTER TABLE #{name} RENAME TO #{new_name}" - end - - def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) #:nodoc: - log(sql,name) do - @connection.execute_update(sql) - end - table = sql.split(" ", 4)[2] - id_value || last_insert_id(table, nil) - end - - def last_insert_id(table, sequence_name) - Integer(select_value("SELECT SEQ FROM SQLITE_SEQUENCE WHERE NAME = '#{table}'")) - end - - def add_limit_offset!(sql, options) #:nodoc: - if options[:limit] - sql << " LIMIT #{options[:limit]}" - sql << " OFFSET #{options[:offset]}" if options[:offset] - end - end - - def tables - @connection.tables.select {|row| row.to_s !~ /^sqlite_/i } - end - - def remove_index(table_name, options = {}) - execute "DROP INDEX #{quote_column_name(index_name(table_name, options))}" - end - - def indexes(table_name, name = nil) - result = select_rows("SELECT name, sql FROM sqlite_master WHERE tbl_name = '#{table_name}' AND type = 'index'", name) - - result.collect do |row| - name = row[0] - index_sql = row[1] - unique = (index_sql =~ /unique/i) - cols = index_sql.match(/\((.*)\)/)[1].gsub(/,/,' ').split - ::ActiveRecord::ConnectionAdapters::IndexDefinition.new(table_name, name, unique, cols) - end - end - end -end diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-0.9.0.1/lib/jdbc_adapter/jdbc_sybase.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-0.9.0.1/lib/jdbc_adapter/jdbc_sybase.rb deleted file mode 100755 index c272590f699..00000000000 --- a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-0.9.0.1/lib/jdbc_adapter/jdbc_sybase.rb +++ /dev/null @@ -1,39 +0,0 @@ -module JdbcSpec - module Sybase - def self.adapter_selector - [/sybase/i, lambda{|cfg,adapt| adapt.extend(JdbcSpec::Sybase)}] - end - - def add_limit_offset!(sql, options) # :nodoc: - @limit = options[:limit] - @offset = options[:offset] - if use_temp_table? - # Use temp table to hack offset with Sybase - sql.sub!(/ FROM /i, ' INTO #artemp FROM ') - elsif zero_limit? - # "SET ROWCOUNT 0" turns off limits, so we havesy - # to use a cheap trick. - if sql =~ /WHERE/i - sql.sub!(/WHERE/i, 'WHERE 1 = 2 AND ') - elsif sql =~ /ORDER\s+BY/i - sql.sub!(/ORDER\s+BY/i, 'WHERE 1 = 2 ORDER BY') - else - sql << 'WHERE 1 = 2' - end - end - end - - # If limit is not set at all, we can ignore offset; - # if limit *is* set but offset is zero, use normal select - # with simple SET ROWCOUNT. Thus, only use the temp table - # if limit is set and offset > 0. - def use_temp_table? - !@limit.nil? && !@offset.nil? && @offset > 0 - end - - def zero_limit? - !@limit.nil? && @limit == 0 - end - - end -end diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-0.9.0.1/lib/jdbc_adapter/missing_functionality_helper.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-0.9.0.1/lib/jdbc_adapter/missing_functionality_helper.rb deleted file mode 100755 index ac730f9590a..00000000000 --- a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-0.9.0.1/lib/jdbc_adapter/missing_functionality_helper.rb +++ /dev/null @@ -1,72 +0,0 @@ -module JdbcSpec - module MissingFunctionalityHelper - #Taken from SQLite adapter - - def alter_table(table_name, options = {}) #:nodoc: - table_name = table_name.to_s.downcase - altered_table_name = "altered_#{table_name}" - caller = lambda {|definition| yield definition if block_given?} - - transaction do - move_table(table_name, altered_table_name, options) - move_table(altered_table_name, table_name, &caller) - end - end - - def move_table(from, to, options = {}, &block) #:nodoc: - copy_table(from, to, options, &block) - drop_table(from) - end - - def copy_table(from, to, options = {}) #:nodoc: - create_table(to, options) do |@definition| - columns(from).each do |column| - column_name = options[:rename] ? - (options[:rename][column.name] || - options[:rename][column.name.to_sym] || - column.name) : column.name - column_name = column_name.to_s - @definition.column(column_name, column.type, - :limit => column.limit, :default => column.default, - :null => column.null) - end - @definition.primary_key(primary_key(from)) - yield @definition if block_given? - end - - copy_table_indexes(from, to) - copy_table_contents(from, to, - @definition.columns, - options[:rename] || {}) - end - - def copy_table_indexes(from, to) #:nodoc: - indexes(from).each do |index| - name = index.name.downcase - if to == "altered_#{from}" - name = "temp_#{name}" - elsif from == "altered_#{to}" - name = name[5..-1] - end - - # index name can't be the same - opts = { :name => name.gsub(/_(#{from})_/, "_#{to}_") } - opts[:unique] = true if index.unique - add_index(to, index.columns, opts) - end - end - - def copy_table_contents(from, to, columns, rename = {}) #:nodoc: - column_mappings = Hash[*columns.map {|col| [col.name, col.name]}.flatten] - rename.inject(column_mappings) {|map, a| map[a.last] = a.first; map} - from_columns = columns(from).collect {|col| col.name} - columns = columns.find_all{|col| from_columns.include?(column_mappings[col.name])} - execute("SELECT * FROM #{from}").each do |row| - sql = "INSERT INTO #{to} ("+columns.map(&:name)*','+") VALUES (" - sql << columns.map {|col| quote(row[column_mappings[col.name]],col)} * ', ' - sql << ')' - execute sql - end - end - end -end diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-0.9.0.1/lib/jdbc_adapter/version.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-0.9.0.1/lib/jdbc_adapter/version.rb deleted file mode 100755 index a276b3b5acc..00000000000 --- a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-0.9.0.1/lib/jdbc_adapter/version.rb +++ /dev/null @@ -1,5 +0,0 @@ -module JdbcAdapter - module Version - VERSION = "0.9.0.1" - end -end diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/.gemtest b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/.gemtest new file mode 100644 index 00000000000..e69de29bb2d --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/.gemtest diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/.specification b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/.specification new file mode 100644 index 00000000000..4f822db8c5a --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/.specification @@ -0,0 +1,282 @@ +--- !ruby/object:Gem::Specification +name: activerecord-jdbc-adapter +version: !ruby/object:Gem::Version + prerelease: false + segments: + - 1 + - 1 + - 3 + version: 1.1.3 +platform: ruby +authors: + - Nick Sieger, Ola Bini and JRuby contributors +autorequire: +bindir: bin +cert_chain: [] + +date: 2011-07-26 00:00:00 +02:00 +default_executable: +dependencies: + - !ruby/object:Gem::Dependency + name: rubyforge + prerelease: false + requirement: &id001 !ruby/object:Gem::Requirement + requirements: + - - ">=" + - !ruby/object:Gem::Version + segments: + - 2 + - 0 + - 4 + version: 2.0.4 + type: :development + version_requirements: *id001 + - !ruby/object:Gem::Dependency + name: hoe + prerelease: false + requirement: &id002 !ruby/object:Gem::Requirement + requirements: + - - ">=" + - !ruby/object:Gem::Version + segments: + - 2 + - 9 + - 4 + version: 2.9.4 + type: :development + version_requirements: *id002 +description: |- + activerecord-jdbc-adapter is a database adapter for Rails' ActiveRecord + component that can be used with JRuby[http://www.jruby.org/]. It allows use of + virtually any JDBC-compliant database with your JRuby on Rails application. +email: nick@nicksieger.com, ola.bini@gmail.com +executables: [] + +extensions: [] + +extra_rdoc_files: + - History.txt + - Manifest.txt + - README.txt + - LICENSE.txt +files: + - History.txt + - Manifest.txt + - README.txt + - Rakefile + - LICENSE.txt + - lib/activerecord-jdbc-adapter.rb + - lib/arjdbc.rb + - lib/jdbc_adapter.rb + - lib/pg.rb + - lib/active_record/connection_adapters/derby_adapter.rb + - lib/active_record/connection_adapters/h2_adapter.rb + - lib/active_record/connection_adapters/hsqldb_adapter.rb + - lib/active_record/connection_adapters/informix_adapter.rb + - lib/active_record/connection_adapters/jdbc_adapter.rb + - lib/active_record/connection_adapters/jndi_adapter.rb + - lib/active_record/connection_adapters/mssql_adapter.rb + - lib/active_record/connection_adapters/mysql2_adapter.rb + - lib/active_record/connection_adapters/mysql_adapter.rb + - lib/active_record/connection_adapters/oracle_adapter.rb + - lib/active_record/connection_adapters/postgresql_adapter.rb + - lib/active_record/connection_adapters/sqlite3_adapter.rb + - lib/arel/engines/sql/compilers/db2_compiler.rb + - lib/arel/engines/sql/compilers/derby_compiler.rb + - lib/arel/engines/sql/compilers/h2_compiler.rb + - lib/arel/engines/sql/compilers/hsqldb_compiler.rb + - lib/arel/engines/sql/compilers/jdbc_compiler.rb + - lib/arel/engines/sql/compilers/mssql_compiler.rb + - lib/arel/visitors/compat.rb + - lib/arel/visitors/db2.rb + - lib/arel/visitors/derby.rb + - lib/arel/visitors/firebird.rb + - lib/arel/visitors/hsqldb.rb + - lib/arel/visitors/sql_server.rb + - lib/arjdbc/db2.rb + - lib/arjdbc/derby.rb + - lib/arjdbc/discover.rb + - lib/arjdbc/firebird.rb + - lib/arjdbc/h2.rb + - lib/arjdbc/hsqldb.rb + - lib/arjdbc/informix.rb + - lib/arjdbc/jdbc.rb + - lib/arjdbc/mimer.rb + - lib/arjdbc/mssql.rb + - lib/arjdbc/mysql.rb + - lib/arjdbc/oracle.rb + - lib/arjdbc/postgresql.rb + - lib/arjdbc/sqlite3.rb + - lib/arjdbc/sybase.rb + - lib/arjdbc/version.rb + - lib/arjdbc/db2/adapter.rb + - lib/arjdbc/derby/adapter.rb + - lib/arjdbc/derby/connection_methods.rb + - lib/arjdbc/firebird/adapter.rb + - lib/arjdbc/h2/adapter.rb + - lib/arjdbc/h2/connection_methods.rb + - lib/arjdbc/hsqldb/adapter.rb + - lib/arjdbc/hsqldb/connection_methods.rb + - lib/arjdbc/informix/adapter.rb + - lib/arjdbc/informix/connection_methods.rb + - lib/arjdbc/jdbc/adapter.rb + - lib/arjdbc/jdbc/callbacks.rb + - lib/arjdbc/jdbc/column.rb + - lib/arjdbc/jdbc/compatibility.rb + - lib/arjdbc/jdbc/connection.rb + - lib/arjdbc/jdbc/connection_methods.rb + - lib/arjdbc/jdbc/core_ext.rb + - lib/arjdbc/jdbc/discover.rb + - lib/arjdbc/jdbc/driver.rb + - lib/arjdbc/jdbc/extension.rb + - lib/arjdbc/jdbc/java.rb + - lib/arjdbc/jdbc/missing_functionality_helper.rb + - lib/arjdbc/jdbc/quoted_primary_key.rb + - lib/arjdbc/jdbc/railtie.rb + - lib/arjdbc/jdbc/rake_tasks.rb + - lib/arjdbc/jdbc/require_driver.rb + - lib/arjdbc/jdbc/type_converter.rb + - lib/arjdbc/mimer/adapter.rb + - lib/arjdbc/mssql/adapter.rb + - lib/arjdbc/mssql/connection_methods.rb + - lib/arjdbc/mssql/limit_helpers.rb + - lib/arjdbc/mssql/tsql_helper.rb + - lib/arjdbc/mysql/adapter.rb + - lib/arjdbc/mysql/connection_methods.rb + - lib/arjdbc/oracle/adapter.rb + - lib/arjdbc/oracle/connection_methods.rb + - lib/arjdbc/postgresql/adapter.rb + - lib/arjdbc/postgresql/connection_methods.rb + - lib/arjdbc/sqlite3/adapter.rb + - lib/arjdbc/sqlite3/connection_methods.rb + - lib/arjdbc/sybase/adapter.rb + - lib/generators/jdbc/jdbc_generator.rb + - lib/jdbc_adapter/rake_tasks.rb + - lib/jdbc_adapter/version.rb + - lib/arjdbc/jdbc/adapter_java.jar + - test/abstract_db_create.rb + - test/db2_simple_test.rb + - test/derby_migration_test.rb + - test/derby_multibyte_test.rb + - test/derby_simple_test.rb + - test/generic_jdbc_connection_test.rb + - test/h2_simple_test.rb + - test/has_many_through.rb + - test/helper.rb + - test/hsqldb_simple_test.rb + - test/informix_simple_test.rb + - test/jdbc_common.rb + - test/jndi_callbacks_test.rb + - test/jndi_test.rb + - test/manualTestDatabase.rb + - test/mssql_db_create_test.rb + - test/mssql_identity_insert_test.rb + - test/mssql_legacy_types_test.rb + - test/mssql_limit_offset_test.rb + - test/mssql_multibyte_test.rb + - test/mssql_simple_test.rb + - test/mysql_db_create_test.rb + - test/mysql_info_test.rb + - test/mysql_multibyte_test.rb + - test/mysql_nonstandard_primary_key_test.rb + - test/mysql_simple_test.rb + - test/oracle_simple_test.rb + - test/oracle_specific_test.rb + - test/pick_rails_version.rb + - test/postgres_db_create_test.rb + - test/postgres_drop_db_test.rb + - test/postgres_information_schema_leak_test.rb + - test/postgres_mixed_case_test.rb + - test/postgres_native_type_mapping_test.rb + - test/postgres_nonseq_pkey_test.rb + - test/postgres_reserved_test.rb + - test/postgres_schema_search_path_test.rb + - test/postgres_simple_test.rb + - test/postgres_table_alias_length_test.rb + - test/simple.rb + - test/sqlite3_simple_test.rb + - test/sybase_jtds_simple_test.rb + - test/activerecord/connection_adapters/type_conversion_test.rb + - test/activerecord/connections/native_jdbc_mysql/connection.rb + - test/db/db2.rb + - test/db/derby.rb + - test/db/h2.rb + - test/db/hsqldb.rb + - test/db/informix.rb + - test/db/jdbc.rb + - test/db/jndi_config.rb + - test/db/logger.rb + - test/db/mssql.rb + - test/db/mysql.rb + - test/db/oracle.rb + - test/db/postgres.rb + - test/db/sqlite3.rb + - test/models/add_not_null_column_to_table.rb + - test/models/auto_id.rb + - test/models/data_types.rb + - test/models/entry.rb + - test/models/mixed_case.rb + - test/models/reserved_word.rb + - test/models/string_id.rb + - test/models/validates_uniqueness_of_string.rb + - lib/arjdbc/jdbc/jdbc.rake + - src/java/arjdbc/db2/DB2RubyJdbcConnection.java + - src/java/arjdbc/derby/DerbyModule.java + - src/java/arjdbc/h2/H2RubyJdbcConnection.java + - src/java/arjdbc/informix/InformixRubyJdbcConnection.java + - src/java/arjdbc/jdbc/AdapterJavaService.java + - src/java/arjdbc/jdbc/JdbcConnectionFactory.java + - src/java/arjdbc/jdbc/RubyJdbcConnection.java + - src/java/arjdbc/jdbc/SQLBlock.java + - src/java/arjdbc/mssql/MssqlRubyJdbcConnection.java + - src/java/arjdbc/mysql/MySQLModule.java + - src/java/arjdbc/mysql/MySQLRubyJdbcConnection.java + - src/java/arjdbc/oracle/OracleRubyJdbcConnection.java + - src/java/arjdbc/postgresql/PostgresqlRubyJdbcConnection.java + - src/java/arjdbc/sqlite3/Sqlite3RubyJdbcConnection.java + - rakelib/compile.rake + - rakelib/db.rake + - rakelib/package.rake + - rakelib/rails.rake + - rakelib/test.rake + - rails_generators/jdbc_generator.rb + - rails_generators/templates/config/initializers/jdbc.rb + - rails_generators/templates/lib/tasks/jdbc.rake + - .gemtest +has_rdoc: true +homepage: http://jruby-extras.rubyforge.org/activerecord-jdbc-adapter +licenses: [] + +post_install_message: +rdoc_options: + - --main + - README.txt + - -SHN + - -f + - darkfish +require_paths: + - lib +required_ruby_version: !ruby/object:Gem::Requirement + requirements: + - - ">=" + - !ruby/object:Gem::Version + segments: + - 0 + version: "0" +required_rubygems_version: !ruby/object:Gem::Requirement + requirements: + - - ">=" + - !ruby/object:Gem::Version + segments: + - 0 + version: "0" +requirements: [] + +rubyforge_project: jruby-extras +rubygems_version: 1.3.6 +signing_key: +specification_version: 3 +summary: JDBC adapter for ActiveRecord, for use within JRuby on Rails. +test_files: [] + + diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/History.txt b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/History.txt new file mode 100644 index 00000000000..f1a92304969 --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/History.txt @@ -0,0 +1,452 @@ +== 1.1.3 + +- Remove AR version < 3 guard around some caching methods (sugg. invadersmustdie) +- Small bug in arjdbc/discover logic, thanks autotelik. +- Added bigint serial support + some testcases for native type mapping (postgres only) +- mssql: use subscript instead of #first. (Kim Toms) +- #71: fix yield called out of block error +- Silence Rake::DSL warnings for Rake > 0.9 + +== 1.1.2 + +- Update version of H2 driver from 1.1.107 to 1.3.153 (Ketan + Padegaonkar, Jeremy Stephens) +- Fix errors in db:test:clone_structure with PostgreSQL (Andrea Campi) +- Fixing limit for sqlServer2000 if primary key is not named 'id' + (Luca Simone) +- DB2: define jdbc_columns (fixes table_exists? bug) (Nick Kreucher) +- ACTIVERECORD_JDBC-152 - omitting limit when dumping bytea fields + (Gregor Schmidt) +- Postgres doesn't support a limit for bytea columns (Alex Tambellini) +- JRUBY-5642: Default to schema public if no schema given for postgres + (Anthony Juckel) +- Sqlite3 supports float data type so use float (Alex Tambellini) +- GH #21: Now using sqlite3 driver from + http://www.xerial.org/trac/Xerial/wiki/SQLiteJDBC (thanks Ukabu) +- GH #65: PG: Respect integer sizes (Alex Tambellini) +- GH #59: PG: Properly escape bytea-escaped string +- GH #53: oracle: allow configuration of schema through schema: key +- GH #50: PG: support multiple schema in search_path (Daniel + Schreiber) +- GH #25: Reload ArJdbc.column_types if number of constants changed +- GH #47: Allow table statistics for indexes to be approximate; speeds + up Oracle +- GH #67: Change primary_keys to use the same catalog/schema/table + separation logic as columns_internal (Marcus Brito). This change + allows set_table_name to specify a custom schema. +- GH #49: mssql: quote table names like column names +- GH #56: mssql: Fix 'select 1' behavior introduced by AR 3.0.7 +- GH #55: Make decimal columns with no precision or scale stay + decimals +- GH #45: Add Arel limit support for Firebird (Systho)) +- GH #39: PG: allow negative integer default values +- GH #19: Make a stub Mysql::Error class +- ACTIVERECORD_JDBC-148: mssql: Ensure regex doesn't match 'from' in a + field name +- GH#31: mssql: Remove extra code breaking mssql w/o limit +- ACTIVERECORD_JDBC-156: mssql: Logic fix for detecting select_count? + +== 1.1.1 + +- Arel 2.0.7 compatibility: fix bugs arising from use of Arel 2.0.7 + + ArJdbc 1.1.0. + - Gracefully handle changes to limit in Arel's AST + - Avoid conflict with Arel 2.0.7's mssql visitor +- Upgrade to PostgreSQL 9.0.801 JDBC drivers (David Kellum) + +== 1.1.0 (12/09/10) + +- Don't narrow platform to '-java' only: revert back to 0.9.2 where + ar-jdbc can be installed under any Ruby (for easier Bundler/Warbler + usage and less confusion on rubygems.org). +- Upgrade MySQL execute code to use RETURN_GENERATED_KEYS. +- Upgrade to MySQL driver version 5.1.13 +- Add multi-statement support, idea from oruen. For databases that + support it, you can now do: + results = Model.connection.execute("select 1; select 2") + and receive back an array of multiple result set arrays. For use with + MySQL, you need to add + options: + allowMultiQueries: true + in database.yml. +- ACTIVERECORD_JDBC-144: Fix limits appearing in schema dump for some + datatypes (Uwe Kubosch) +- Fixes for DB2 limit/offset +- Fix rake db:create for 'jdbc' adapter (Joeri Samson) +- add create/drop database methods to h2 adapter (sahglie) +- Use connection getDatabaseProductName instead of getClass.getName + when detecting JNDI dialects (Denis Odorcic) +- ACTIVERECORD_JDBC-146: Fix create_table to not append encoding (Marc Slemko) +- All green on SQLite3 Rails master ActiveRecord tests +- ACTIVERECORD_JDBC-140: Sync postgres add/change column code from Rails master +- ACTIVERECORD_JDBC-139: TEXT/DATE on PostgreSQL should not have limits + +== 1.0.3 (11/29/10) + +- ACTIVERECORD_JDBC-143: Implement table_exists? fixing association + table names with schema prefixes +- Cleanup of column code for hsqldb (Denis Odorcic) +- Rails 3.0.3 support - add Arel 2 visitors for all adapters +- Fix MySQL date types to not have limits (Chris Lowder) +- ACTIVERECORD_JDBC-141: Better schema support in H2 + +== 1.0.2 + +- ACTIVERECORD_JDBC-134: Fix conflicting adapter/column superclasses +- ACTIVERECORD_JDBC-135: Fix regression on PG with boolean and :limit +- Slew of Derby fixes courtesy of Denis Odorcic + +== 1.0.1 + +- Fix db:test:purge issue affecting all adapters in 1.0.0 due to + incorrect fix to JRUBY-5081 in 8b4b9c5 + +== 1.0.0 + +- Thanks to David Kellum, Dmitry Denisov, Dwayne Litzenberger, Gregor + Schmidt, James Walker, John Duff, Joshua Suggs, Nicholas J Kreucher, + Peter Donald, Geoff Longman, Uwe Kubosch, Youhei Kondou, Michael + Pitman, Alex B, and Ryan Bell for their contributions to this + release. +- BIG set of DB2 updates (Thanks Nick Kreucher) +- Deprecate jdbc_adapter/rake_tasks +- (1.0.0.beta1) +- Make database-specific extensions only load when necessary +- Allow for discovery of database extensions outside of ar-jdbc + proper. This should allow for custom database development to be + developed and released without relying on AR-JDBC core. +- Get AR's own tests running as close to 100% as possible. MySQL is + currently 100%, SQLite3 is close. +- JRUBY-4876: Bump up Derby's max index name length (Uwe Kubosch) +- (1.0.0.beta2) +- 98 commits since beta1 +- MSSQL updates from dlitz and realityforge +- ACTIVERECORD_JDBC-131: Fix string slug issue for DB2 (Youhei Kondou) +- JRUBY-1642: Don't use H2 INFORMATION_SCHEMA in table or column + searches +- JRUBY-4972: Attempt to deal with type(0)/:limit => 0 by not setting + it808e213 +- JRUBY-5040: Fix issue with limits on timestamps in MySQL +- JRUBY-3555: Allow setting Derby schema with 'schema:' option +- ACTIVERECORD_JDBC-98: Make sure we actuall raise an error when + inappropriately configured +- ACTIVERECORD_JDBC-112: Add schema dumper tests for already-fixed + MySQL type limits +- ACTIVERECORD_JDBC-113: Fix PG float precision issue +- ACTIVERECORD_JDBC-103: Fix decimal options for PG add/change column + (Michael Pitman) +- ACTIVERECORD_JDBC-127: Fix quoting of Date vs. Time(stamp) for + Oracle (Lenny Marks) +- Oracle: Sort out the NUMBER vs NUMBER(x) vs NUMBER(x,y) situation. +- JRUBY-3051: Think we finally got the PG mixed-case patches applied. +- JRUBY-5081: Consolidate code for dropping DB via postgres +- ACTIVERECORD_JDBC-101: Add override of LONGVARCHAR => CLOB for + informix +- ACTIVERECORD_JDBC-107: Fix MySQL update_all issue on AR 2.3 +- ACTIVERECORD_JDBC-124: Filter out special _row_num column +- ACTIVERECORD_JDBC-126: Fix sql 2000 limit/offset per Michael Pitman +- ACTIVERECORD_JDBC-125: Add tweak to limit/offset code for HABTM + queries (alex b) +- ACTIVERECORD_JDBC-129: Don't have limits for text, binary or bit + fields +- (1.0.0 final) +- Fix a few more SQLite3 AR tests +- SQLite3: handle ":memory:" database +- Release new SQLite3 driver 3.6.14.2 and new Derby driver 10.6.2.1 + +== 0.9.7 + +- JRUBY-4781: Fix multiple database connection collision issue w/ + Oracle +- ACTIVERECORD_JDBC-115: Support SAVEPOINTS for MySQL and PG so that + nested transactions can be faked +- ACTIVERECORD_JDBC-116: Handle schema.table better for MySQL (thanks + Dilshod Mukhtarov) +- Fix 'Wrong # of arguments (2 for 1)' issue with #create_database for + MySQL and AR 3.0 +- SQLServer 2000 support (thanks Jay McGaffigan) + +== 0.9.6 + +- The Oracle release! +- Oracle should be working much better with this release. Also updated + to work with Rails 3. +- Get all unit tests running cleanly on Oracle, fixing previous + datetime/timezone issues. +- ACTIVERECORD_JDBC-83: Add :sequence_start_value option to + create_table, following oracle_enhanced adapter +- ACTIVERECORD_JDBC-33: Don't double-quote table names in oracle +- ACTIVERECORD_JDBC-17: Fix Oracle primary keys so that /^NUMBER$/ => :integer +- Fix remaining blockers ACTIVERECORD_JDBC-82, JRUBY-3675, + ACTIVERECORD_JDBC-22, ACTIVERECORD_JDBC-27, JRUBY-4759 + +== 0.9.5 + +- The MSSQL release, courtesy of Mike Williams and Lonely + Planet. +- JRuby + AR-JDBC is now seen as the hassle-free way of using Rails + with SQLServer! +- Many fixes for MSSQL, including ACTIVERECORD_JDBC-18, + ACTIVERECORD_JDBC-41, ACTIVERECORD_JDBC-56, ACTIVERECORD_JDBC-94, + ACTIVERECORD_JDBC-99, JRUBY-3805, JRUBY-3793, JRUBY-4221 +- All tests pass on Rails 3.0.0.beta3! + +== 0.9.4 + +- ACTIVERECORD_JDBC-96: DB2 JdbcSpec cannot dump schema correctly + (Youhei Kondou) +- ACTIVERECORD_JDBC-97: Dont use Rails 3 deprecated constants (David + Calavera) +- Updates for rake db:schema:dump compatibility with Rails 2.3+ and + MySQL (Joakim Kolsjö) +- Rails 3.0.0.beta2 compatibility +- Return of Derby, H2, Hsqldb support (requires AR >= 3.0.0.beta2) + +== 0.9.3 + +- Rails 3 compatibility +- PLEASE NOTE: ActiveRecord in Rails 3 has changed in a way that + doesn't allow non-standard DBs (such as the Derby and H2 embedded + DBs) to work. We're investigating the effort required to support + these databases and hope to have something for a future release. +- ACTIVERECORD_JDBC-91: Fix schema search path for PostgreSQL (Alex + Kuebo) +- ACTIVERECORD_JDBC-87: DB2 ID insert fix (Youhei Kondou) +- ACTIVERECORD_JDBC-90: MSSQL fix for DATEs (jlangenauer) +- ACTIVERECORD_JDBC-93: Fix string IDs for sqlite3, hsql/h2 (moser) +- ACTIVERECORD_JDBC-86: Fix Derby queries starting with VALUES (Dwayne Litzenberger) +- ACTIVERECORD_JDBC-95: Fix INSERT ... RETURNING for PostgreSQL + +== 0.9.2 + +- The main, highly awaited fix for this release is a solution to the + rake db:create/db:drop issue. The main change is a new 'jdbc' rails + generator that should be run once to prepare a Rails application to + use JDBC. The upside of this generator is that you no longer will + need to alter database.yml for JDBC. See the README.txt for details. +- Cleanup and reconnect if errors occur during begin/rollback + (Jean-Dominique Morani, Christian Seiler) +- ACTIVERECORD_JDBC-1: Add #drop_database method for oracle (does the + same thing as recreate_database) +- Sqlite3 and MSSQL fixes (Jean-Dominique Morani) +- JRUBY-3512: Treat LONGVARCHAR as a CLOB for Mssql +- JRUBY-3624: Upgrade Derby to 10.5.3.0 and add native limit/offset + support (Christopher Saunders) +- JRUBY-3616: Fix postgres non-sequence primary keys (David Kellum) +- JRUBY-3669: Fix Oracle case with unconfigured schema (Dan Powell) +- Fixed quote_column_name of jdbc_oracle to accept numbers (Marcelo + Murad) +- Fix for mysql tables with non standard primary keys such that the + schema dump is correct (Nick Zalabak) +- MSSQL fixes from Mike Luu: + - add support for MSSQL uniqueidentifier datatype + - always quote strings using unicode identifier for MSSQL +- Changes primary_key generation to use always instead of by default + for DB2 (Amos King) +- Improves the SQLite adapter by fixing rename_column, change_column, + change_column_default, changing remove_column, and adding + remove_columns (Ryan Baumann) +- More oracle love courtesy Ben Browning and Jens Himmelreich +- JRUBY-3608: Add missing change_column_null method for postgres +- JRUBY-3508: Fix quoting of integer and float columns + +== 0.9.1 + +- We did a lot of internal cleanup this release in the hopes of + simplifying the code and increasing performance. +- Many SQLite updates (thanks Nils Christian Haugen) +- JRUBY-2912: Fix MSSQL create/drop database (Joern Hartmann) +- JRUBY-2767: Mistake in selecting identity with H2/HSQLDB +- JRUBY-2884: jdbc_postgre.rb issue handling nil booleans (also a fix + for hsqldb/h2) + tests +- JRUBY-2995: activerecord jdbc derby adapter should quote columns + called 'year' +- JRUBY-2897: jdbc_postgre.rb needs microsecond support +- JRUBY-3282: Upgrade to derby 10.4.2.0 to allow unique constraints + with nullable columns +- Update h2 from 1.0.63 to 1.1.107 in driver +- JRUBY-3026: [Derby] Allow select/delete/update conditions with + comparison to NULL using '=' +- JRUBY-2996: ...(actually this fixes only remaining issue of this bug + which was symbols making into quote were exploding +- JRUBY-2691: Update sybase driver to pass simple unit tests with jtds + and verify it works with the new dialect keyword. patch by Leigh + Kennedy +- Make :float type work on h2,hsql [returned as string]. Make :float + work on hsqldb (no paren value supported). Make REAL_TYPE just + return RubyFloat +- JRUBY-3222: Upgrade #type_to_sql to variation of AR 2.1.2 version +- Add patch supplied in JRUBY-3489 (patch by Jean-Dominique Morani) +- Various Oracle fixes by edsono +- JRUBY-2688: Don't hard-code MySQL connection character encoding to + utf8 + +== 0.9 + +- Now updated to support ActiveRecord 2.2. JNDI-based connections will + automatically connect/disconnect for every AR connection pool + checkout/checkin. For best results, set your pool: parameter >= the + actual maximum size of the JNDI connection pool. (We'll look at how + to eliminate the need to configure AR's pool in the future.) +- NEW! Informix support courtesy of Javier Fernandez-Ivern. +- Backport another Oracle CLOB issue, thanks Edson César. +- Rubyforge #22018: chomp final trailing semicolon for oracle +- JRUBY-2848: Fix NPE error in set_native_database_types +- Rework oracle lob saving callback to be Rails 2.1 friendly (assist + from court3nay) +- JRUBY-2715: Add create/drop database methods to Postgres (Peter Williams) +- JRUBY-3183: Fix structure dump for Postgres (Ryan Bell) +- JRUBY-3184: recreate_database for test database working for PG (Ryan Bell) +- JRUBY-3186: disable referential integrity for PG (Ryan Bell) +- Authoritative repository now hosted at + git://github.com/nicksieger/activerecord-jdbc-adapter.git; rubyforge + svn trunk cleaned out. + +== 0.8.2 + +- Added an optional config key called :dialect. Using :dialect allows you to + override the default SQL dialect for the driver class being used. There are + a few cases for this: + - Using using Sybase w/ the jTDS driver. + - Using rebranded drivers. + - It makes more sense to use :dialect, rather then :driver when using JNDI. +- JRUBY-2619: Typo with :test config causing problems with dev database (Igor Minar) +- 20524, JRUBY-2612: Since when did I think that there was a #true? method on Object? + +== 0.8.1 + +- Now sporting a JDBC sqlite3 adapter! Thanks Joseph Athman. +- Added support for InterSystems Cache database (Ryan Bell) +- Fix for JRUBY-2256 +- JRUBY-1638, JRUBY-2404, JRUBY-2463: schema.table handling and Oracle NUMBER fixes (Darcy Schultz & Jesse Hu) +- Add structure dump and other DDL-ish for DB2 (courtesy abedra and stuarthalloway) +- Fix missing quote_table_name function under Rails 1.2.6 and earlier +- Small tweaks to jdbc.rake to select proper config +- JRUBY-2011: Fix MSSQL string un-quoting issue (Silvio Fonseca) +- JRUBY-1977, 17427: Fix information_schema select issue with MSSQL (Matt Burke) +- 20479: Improve get_table_name for MSSQL (Aslak Hellesøy) +- 20243: numerics improvements for MSSQL (Aslak Hellesøy) +- 20172: don't quote table names for MSSQL (Thor Marius Henrichsen) +- 19729: check for primary key existence in postgres during insert (Martin Luder) +- JRUBY-2297, 18846: retrying failing SQL statements is harmful when not autocommitting (Craig McMillan) +- 10021: very preliminary sybase support. (Mark Atkinson) Not usable until collision w/ sqlserver driver is resolved. +- JRUBY-2312, JRUBY-2319, JRUBY-2322: Oracle timestamping issues (Jesse Hu & Michael König) +- JRUBY-2422: Fix MySQL referential integrity and rollback issues +- JRUBY-2382: mysql string quoting fails with ArrayIndexOutofBoundsException + +== 0.8 + +- NOTE: This release is only compatible with JRuby 1.1RC3 or later. +- Because of recent API changes in trunk in preparation for JRuby 1.1, this release is not + backward compatible with previous JRuby releases. Hence the version bump. +- Internal: convert Java methods to be defined with annotations +- Fix problem with reserved words coming back pre-quoted from #indexes in postgres +- JRUBY-2205: Fix N^2 allocation of bytelists for mysql quoting (taw) +- Attempt a fix for Rubyforge 18059 +- Upgrade derby to 10.3.2.1 +- Fix db:create etc. in the case where JDBC is loaded in Rails' preinitializer.rb +- Fix db:drop to actually work +- Fix for Rubyforge #11567 (Matt Williams) + +== 0.7.2 + +- JRUBY-1905: add_column for derby, hsqldb, and postgresql (Stephen Bannasch) +- Fix db:create for JDBC +- Support Rails 2 with the old "require 'jdbc_adapter'" approach +- JRUBY-1966: Instead of searching for just tables, search for views and tables. +- JRUBY-1583: DB2 numeric quoting (Ryan Shillington) +- JRUBY-1634: Oracle DATE type mapping (Daniel Wintschel) +- JRUBY-1543: rename_column issue with more recent MySQL drivers (Oliver Schmelzle) +- Rubyforge #15074: ConnectionAdapters::JdbcAdapter.indexes is missing name and + schema_name parameters in the method signature (Igor Minar) +- Rubyforge #13558: definition for the indexes method (T Meyarivan) +- JRUBY-2051: handle schemaname and tablename more correctly for columns +- JRUBY-2102: Postgres Adapter cannot handle datetime type (Rainer Hahnekamp) +- JRUBY-2018: Oracle behind ActiveRecord-JDBC fails with "Invalid column index" (K Venkatasubramaniyan) +- JRUBY-2012: jdbc_mysql structure dump fails for mysql views (Tyler Jennings) + +== 0.7.1 + +- Add adapter and driver for H2 courtesy of Caleb Land +- Fix "undefined method `last' for {}:Hash" error introduced with new Rake 0.8.1 (JRUBY-1859) + +== 0.7 + +- PLEASE NOTE: This release is not compatible with JRuby releases earlier than + 1.0.3 or 1.1b2. If you must use JRuby 1.0.2 or earlier, please install the + 0.6 release. +- Release coincides with JRuby 1.0.3 and JRuby 1.1b2 releases +- Simultaneous support for JRuby trunk and 1.0 branch +- Get rid of log_no_bench method, so we time SQL execution again. +- Implement #select_rows +- MySQL migration and quoting updates + +== 0.6 + +- Gem is renamed to "activerecord-jdbc-adapter" to follow new conventions + introduced in Rails 2.0 for third-party adapters. Rails 2.0 compatibility is + introduced. +- Add dependency on ActiveRecord >= 1.14 (from the Rails 1.1.x release) +- New drivers (jdbc-XXX) and adapter (activerecord-jdbcXXX-adapter) gems + available separately. See the README.txt file for details. +- Plain "jdbc" driver is still available if you want to use the full + driver/url way of specifying the driver. +- More bugfixes to Oracle and SQLServer courtesy of Ola & ThoughtWorks + +== 0.5 + +- Release coincides with JRuby 1.0.1 release +- It is no longer necessary to specify :driver and :url configuration + parameters for the mysql, postgresql, oracle, derby, hsqldb, and h2 + adapters. The previous configuration is still valid and compatible, but for + new applications, this makes it possible to use the exact same database.yml + configuration as Rails applications running under native Ruby. +- JDBC drivers can now be dynamically loaded by Ruby code, without being on + the classpath prior to launching JRuby. Simply use "require + 'jdbc-driver.jar'" in JRuby code to add it to the runtime classpath. +- Updates to HSQL, MS SQLServer, Postgres, Oracle and Derby adapters + +== 0.4 + +- Release coincides with JRuby 1.0 release +- Shoring up PostgreSQL (courtesy Dudley Flanders) and HSQL (courtesy Matthew + Williams) +- Fix timestamps on Oracle to use DATE (as everything else) +- Derby fixes: Fix for open result set issue, better structure dump, quoting, + column type changing +- Sybase type recognition fix (courtesy Dean Mao) + +== 0.3.1 + +- Derby critical fixes shortly after 0.3 + +== 0.3 + +- Release coincides with JRuby 1.0.0RC1 release +- Improvements for Derby, Postgres, and Oracle, all of which are running + > 95% of AR tests + +== 0.2.4 + +- Release coincides with JRuby 0.9.9 release +- JRuby 0.9.9 is required +- MySQL close to 100% working +- Derby improvements +- DECIMAL/NUMERIC/FLOAT/REAL bugs fixed with type recognition for Oracle, + Postgres, etc. +- HSQLDB has regressed this release and may not be functioning; we'll get it + fixed for the next one + +== 0.2.3 + +- Release coincides (and compatible) with JRuby 0.9.8 release +- 8 bugs fixed: see http://rubyurl.com/0Da +- Improvements and compatibility fixes for Rails 1.2.x + +== 0.2.1, 0.2.2 + +- Early releases, added better support for multiple databases + +== 0.0.1 + +- Initial, very alpha release diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-0.9.0.1/LICENSE.txt b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/LICENSE.txt index a1a5a8c1163..a1a5a8c1163 100755..100644 --- a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-0.9.0.1/LICENSE.txt +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/LICENSE.txt diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/Manifest.txt b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/Manifest.txt new file mode 100644 index 00000000000..79054a8d699 --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/Manifest.txt @@ -0,0 +1,187 @@ +History.txt +Manifest.txt +README.txt +Rakefile +LICENSE.txt +lib/activerecord-jdbc-adapter.rb +lib/arjdbc.rb +lib/jdbc_adapter.rb +lib/pg.rb +lib/active_record/connection_adapters/derby_adapter.rb +lib/active_record/connection_adapters/h2_adapter.rb +lib/active_record/connection_adapters/hsqldb_adapter.rb +lib/active_record/connection_adapters/informix_adapter.rb +lib/active_record/connection_adapters/jdbc_adapter.rb +lib/active_record/connection_adapters/jndi_adapter.rb +lib/active_record/connection_adapters/mssql_adapter.rb +lib/active_record/connection_adapters/mysql2_adapter.rb +lib/active_record/connection_adapters/mysql_adapter.rb +lib/active_record/connection_adapters/oracle_adapter.rb +lib/active_record/connection_adapters/postgresql_adapter.rb +lib/active_record/connection_adapters/sqlite3_adapter.rb +lib/arel/engines/sql/compilers/db2_compiler.rb +lib/arel/engines/sql/compilers/derby_compiler.rb +lib/arel/engines/sql/compilers/h2_compiler.rb +lib/arel/engines/sql/compilers/hsqldb_compiler.rb +lib/arel/engines/sql/compilers/jdbc_compiler.rb +lib/arel/engines/sql/compilers/mssql_compiler.rb +lib/arel/visitors/compat.rb +lib/arel/visitors/db2.rb +lib/arel/visitors/derby.rb +lib/arel/visitors/firebird.rb +lib/arel/visitors/hsqldb.rb +lib/arel/visitors/sql_server.rb +lib/arjdbc/db2.rb +lib/arjdbc/derby.rb +lib/arjdbc/discover.rb +lib/arjdbc/firebird.rb +lib/arjdbc/h2.rb +lib/arjdbc/hsqldb.rb +lib/arjdbc/informix.rb +lib/arjdbc/jdbc.rb +lib/arjdbc/mimer.rb +lib/arjdbc/mssql.rb +lib/arjdbc/mysql.rb +lib/arjdbc/oracle.rb +lib/arjdbc/postgresql.rb +lib/arjdbc/sqlite3.rb +lib/arjdbc/sybase.rb +lib/arjdbc/version.rb +lib/arjdbc/db2/adapter.rb +lib/arjdbc/derby/adapter.rb +lib/arjdbc/derby/connection_methods.rb +lib/arjdbc/firebird/adapter.rb +lib/arjdbc/h2/adapter.rb +lib/arjdbc/h2/connection_methods.rb +lib/arjdbc/hsqldb/adapter.rb +lib/arjdbc/hsqldb/connection_methods.rb +lib/arjdbc/informix/adapter.rb +lib/arjdbc/informix/connection_methods.rb +lib/arjdbc/jdbc/adapter.rb +lib/arjdbc/jdbc/callbacks.rb +lib/arjdbc/jdbc/column.rb +lib/arjdbc/jdbc/compatibility.rb +lib/arjdbc/jdbc/connection.rb +lib/arjdbc/jdbc/connection_methods.rb +lib/arjdbc/jdbc/core_ext.rb +lib/arjdbc/jdbc/discover.rb +lib/arjdbc/jdbc/driver.rb +lib/arjdbc/jdbc/extension.rb +lib/arjdbc/jdbc/java.rb +lib/arjdbc/jdbc/missing_functionality_helper.rb +lib/arjdbc/jdbc/quoted_primary_key.rb +lib/arjdbc/jdbc/railtie.rb +lib/arjdbc/jdbc/rake_tasks.rb +lib/arjdbc/jdbc/require_driver.rb +lib/arjdbc/jdbc/type_converter.rb +lib/arjdbc/mimer/adapter.rb +lib/arjdbc/mssql/adapter.rb +lib/arjdbc/mssql/connection_methods.rb +lib/arjdbc/mssql/limit_helpers.rb +lib/arjdbc/mssql/tsql_helper.rb +lib/arjdbc/mysql/adapter.rb +lib/arjdbc/mysql/connection_methods.rb +lib/arjdbc/oracle/adapter.rb +lib/arjdbc/oracle/connection_methods.rb +lib/arjdbc/postgresql/adapter.rb +lib/arjdbc/postgresql/connection_methods.rb +lib/arjdbc/sqlite3/adapter.rb +lib/arjdbc/sqlite3/connection_methods.rb +lib/arjdbc/sybase/adapter.rb +lib/generators/jdbc/jdbc_generator.rb +lib/jdbc_adapter/rake_tasks.rb +lib/jdbc_adapter/version.rb +lib/arjdbc/jdbc/adapter_java.jar +test/abstract_db_create.rb +test/db2_simple_test.rb +test/derby_migration_test.rb +test/derby_multibyte_test.rb +test/derby_simple_test.rb +test/generic_jdbc_connection_test.rb +test/h2_simple_test.rb +test/has_many_through.rb +test/helper.rb +test/hsqldb_simple_test.rb +test/informix_simple_test.rb +test/jdbc_common.rb +test/jndi_callbacks_test.rb +test/jndi_test.rb +test/manualTestDatabase.rb +test/mssql_db_create_test.rb +test/mssql_identity_insert_test.rb +test/mssql_legacy_types_test.rb +test/mssql_limit_offset_test.rb +test/mssql_multibyte_test.rb +test/mssql_simple_test.rb +test/mysql_db_create_test.rb +test/mysql_info_test.rb +test/mysql_multibyte_test.rb +test/mysql_nonstandard_primary_key_test.rb +test/mysql_simple_test.rb +test/oracle_simple_test.rb +test/oracle_specific_test.rb +test/pick_rails_version.rb +test/postgres_db_create_test.rb +test/postgres_drop_db_test.rb +test/postgres_information_schema_leak_test.rb +test/postgres_mixed_case_test.rb +test/postgres_native_type_mapping_test.rb +test/postgres_nonseq_pkey_test.rb +test/postgres_reserved_test.rb +test/postgres_schema_search_path_test.rb +test/postgres_simple_test.rb +test/postgres_table_alias_length_test.rb +test/simple.rb +test/sqlite3_simple_test.rb +test/sybase_jtds_simple_test.rb +test/activerecord/connection_adapters/type_conversion_test.rb +test/activerecord/connections/native_jdbc_mysql/connection.rb +test/db/db2.rb +test/db/derby.rb +test/db/h2.rb +test/db/hsqldb.rb +test/db/informix.rb +test/db/jdbc.rb +test/db/jndi_config.rb +test/db/logger.rb +test/db/mssql.rb +test/db/mysql.rb +test/db/oracle.rb +test/db/postgres.rb +test/db/sqlite3.rb +test/models/add_not_null_column_to_table.rb +test/models/auto_id.rb +test/models/data_types.rb +test/models/entry.rb +test/models/mixed_case.rb +test/models/reserved_word.rb +test/models/string_id.rb +test/models/validates_uniqueness_of_string.rb +lib/arjdbc/jdbc/jdbc.rake +src/java/arjdbc/db2/DB2RubyJdbcConnection.java +src/java/arjdbc/derby/DerbyModule.java +src/java/arjdbc/h2/H2RubyJdbcConnection.java +src/java/arjdbc/informix/InformixRubyJdbcConnection.java +src/java/arjdbc/jdbc/AdapterJavaService.java +src/java/arjdbc/jdbc/JdbcConnectionFactory.java +src/java/arjdbc/jdbc/RubyJdbcConnection.java +src/java/arjdbc/jdbc/SQLBlock.java +src/java/arjdbc/mssql/MssqlRubyJdbcConnection.java +src/java/arjdbc/mysql/MySQLModule.java +src/java/arjdbc/mysql/MySQLRubyJdbcConnection.java +src/java/arjdbc/oracle/OracleRubyJdbcConnection.java +src/java/arjdbc/postgresql/PostgresqlRubyJdbcConnection.java +src/java/arjdbc/sqlite3/Sqlite3RubyJdbcConnection.java +rakelib/compile.rake +rakelib/db.rake +rakelib/package.rake +rakelib/rails.rake +rakelib/test.rake +rails_generators/jdbc_generator.rb +rails_generators/templates +rails_generators/templates/config +rails_generators/templates/lib +rails_generators/templates/config/initializers +rails_generators/templates/config/initializers/jdbc.rb +rails_generators/templates/lib/tasks +rails_generators/templates/lib/tasks/jdbc.rake diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/README.txt b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/README.txt new file mode 100644 index 00000000000..a13afdfc730 --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/README.txt @@ -0,0 +1,181 @@ +activerecord-jdbc-adapter is a database adapter for Rails' ActiveRecord +component that can be used with JRuby[http://www.jruby.org/]. It allows use of +virtually any JDBC-compliant database with your JRuby on Rails application. + +== Databases + +Activerecord-jdbc-adapter provides full or nearly full support for: +MySQL, PostgreSQL, SQLite3, Oracle, Microsoft SQL Server, DB2, +FireBird, Derby, HSQLDB, H2, and Informix. + +Other databases will require testing and likely a custom configuration module. +Please join the activerecord-jdbc +mailing-lists[http://kenai.com/projects/activerecord-jdbc/lists] to help us discover +support for more databases. + +== Using ActiveRecord JDBC + +=== Inside Rails + +To use activerecord-jdbc-adapter with JRuby on Rails: + +1. Choose the adapter you wish to gem install. The following pre-packaged +adapters are available: + + * base jdbc (<tt>activerecord-jdbc-adapter</tt>). Supports all available databases via JDBC, but requires you to download and manually install the database vendor's JDBC driver .jar file. + * mysql (<tt>activerecord-jdbcmysql-adapter</tt>) + * postgresql (<tt>activerecord-jdbcpostgresql-adapter</tt>) + * sqlite3 (<tt>activerecord-jdbcsqlite3-adapter</tt>) + * derby (<tt>activerecord-jdbcderby-adapter</tt>) + * hsqldb (<tt>activerecord-jdbchsqldb-adapter</tt>) + * h2 (<tt>activerecord-jdbch2-adapter</tt>) + * mssql (<tt>activerecord-jdbcmssql-adapter</tt>) + +2a. For Rails 3, if you're generating a new application, use the +following command to generate your application: + + jruby -S rails new sweetapp -m http://jruby.org/rails3.rb + +2b. Otherwise, you'll need to perform some extra configuration steps +to prepare your Rails application for JDBC. + +If you're using Rails 3, you'll need to modify your Gemfile to use the +activerecord-jdbc-adapter gem under JRuby. Change your Gemfile to look +like the following (using sqlite3 as an example): + + if defined?(JRUBY_VERSION) + gem 'activerecord-jdbc-adapter' + gem 'jdbc-sqlite3' + else + gem 'sqlite3-ruby', :require => 'sqlite3' + end + +If you're using Rails 2: + + jruby script/generate jdbc + +3. Configure your database.yml in the normal Rails style. + +Legacy configuration: If you use one of the convenience +'activerecord-jdbcXXX-adapter' adapters, you can still put a 'jdbc' +prefix in front of the database adapter name as below. + + development: + adapter: jdbcmysql + username: blog + password: + hostname: localhost + database: weblog_development + +For other databases, you'll need to know the database driver class and +URL. Example: + + development: + adapter: jdbc + username: blog + password: + driver: com.mysql.jdbc.Driver + url: jdbc:mysql://localhost:3306/weblog_development + + For JNDI data sources, you may simply specify the JNDI location as follows + (the adapter will be automatically detected): + + production: + adapter: jdbc + jndi: jdbc/mysqldb + +=== Standalone, with ActiveRecord + +1. Install the gem with JRuby: + + jruby -S gem install activerecord-jdbc-adapter + +If you wish to use the adapter for a specific database, you can +install it directly and a driver gem will be installed as well: + + jruby -S gem install activerecord-jdbcderby-adapter + +2. After this you can establish a JDBC connection like this: + + ActiveRecord::Base.establish_connection( + :adapter => 'jdbcderby', + :database => "db/my-database" + ) + +or like this (but requires that you manually put the driver jar on the classpath): + + ActiveRecord::Base.establish_connection( + :adapter => 'jdbc', + :driver => 'org.apache.derby.jdbc.EmbeddedDriver', + :url => 'jdbc:derby:test_ar;create=true' + ) + +== Extending AR-JDBC + +You can create your own extension to AR-JDBC for a JDBC-based database +that core AR-JDBC does not support. We've created an example project +for the Intersystems Cache database that you can examine as a +template. See the project for more information at the following URL: + + http://github.com/nicksieger/activerecord-cachedb-adapter + +== Getting the source + +The source for activerecord-jdbc-adapter is available using git. + + git clone git://github.com/nicksieger/activerecord-jdbc-adapter.git + +== Feedback + +Please file bug reports at +http://kenai.com/jira/browse/ACTIVERECORD_JDBC. If you're not sure if +something's a bug, feel free to pre-report it on the mailing lists. + +== Project Info + +* Mailing Lists: http://kenai.com/projects/activerecord-jdbc/lists +* Issues: http://kenai.com/jira/browse/ACTIVERECORD_JDBC +* Source: + git://github.com/nicksieger/activerecord-jdbc-adapter.git + git://kenai.com/activerecord-jdbc~main + +== Running AR-JDBC's Tests + +Drivers for 6 open-source databases are included. Provided you have +MySQL installed, you can simply type <tt>jruby -S rake</tt> to run the +tests. A database named <tt>weblog_development</tt> is needed +beforehand with a connection user of "blog" and an empty password. You +alse need to grant "blog" create privileges on +'test_rake_db_create.*'. + +If you also have PostgreSQL available, those tests will be run if the +`psql' executable can be found. Also ensure you have a database named +<tt>weblog_development</tt> and a user named "blog" and an empty +password. + +If you want rails logging enabled during these test runs you can edit +test/jdbc_common.rb and add the following line: + +require 'db/logger' + +== Running AR Tests + +To run the current AR-JDBC sources with ActiveRecord, just use the +included "rails:test" task. Be sure to specify a driver and a path to +the ActiveRecord sources. + + jruby -S rake rails:test DRIVER=mysql RAILS=/path/activerecord_source_dir + +== Authors + +This project was written by Nick Sieger <nick@nicksieger.com> and Ola Bini +<olabini@gmail.com> with lots of help from the JRuby community. + +== License + +activerecord-jdbc-adapter is released under a BSD license. See the LICENSE file +included with the distribution for details. + +Open-source driver gems for activerecord-jdbc-adapter are licensed under the +same license the database's drivers are licensed. See each driver gem's +LICENSE.txt file for details. diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/Rakefile b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/Rakefile new file mode 100644 index 00000000000..3181c30ab76 --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/Rakefile @@ -0,0 +1,10 @@ +require 'rake/testtask' +require 'rake/clean' +CLEAN.include 'derby*', 'test.db.*','test/reports', 'test.sqlite3','lib/**/*.jar','manifest.mf', '*.log' + +task :default => [:java_compile, :test] + +task :filelist do + puts FileList['pkg/**/*'].inspect +end + diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/active_record/connection_adapters/derby_adapter.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/active_record/connection_adapters/derby_adapter.rb new file mode 100644 index 00000000000..9ca9c42d116 --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/active_record/connection_adapters/derby_adapter.rb @@ -0,0 +1 @@ +require 'arjdbc/derby' diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/active_record/connection_adapters/h2_adapter.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/active_record/connection_adapters/h2_adapter.rb new file mode 100644 index 00000000000..ba3f8abf327 --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/active_record/connection_adapters/h2_adapter.rb @@ -0,0 +1 @@ +require 'arjdbc/h2' diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/active_record/connection_adapters/hsqldb_adapter.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/active_record/connection_adapters/hsqldb_adapter.rb new file mode 100644 index 00000000000..2f31a77f1a8 --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/active_record/connection_adapters/hsqldb_adapter.rb @@ -0,0 +1 @@ +require 'arjdbc/hsqldb' diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/active_record/connection_adapters/informix_adapter.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/active_record/connection_adapters/informix_adapter.rb new file mode 100644 index 00000000000..331c057d699 --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/active_record/connection_adapters/informix_adapter.rb @@ -0,0 +1 @@ +require 'arjdbc/informix' diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/active_record/connection_adapters/jdbc_adapter.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/active_record/connection_adapters/jdbc_adapter.rb new file mode 100644 index 00000000000..f81c6b436ee --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/active_record/connection_adapters/jdbc_adapter.rb @@ -0,0 +1 @@ +require 'arjdbc/jdbc' diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/active_record/connection_adapters/jndi_adapter.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/active_record/connection_adapters/jndi_adapter.rb new file mode 100644 index 00000000000..f81c6b436ee --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/active_record/connection_adapters/jndi_adapter.rb @@ -0,0 +1 @@ +require 'arjdbc/jdbc' diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/active_record/connection_adapters/mssql_adapter.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/active_record/connection_adapters/mssql_adapter.rb new file mode 100644 index 00000000000..525a9a31225 --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/active_record/connection_adapters/mssql_adapter.rb @@ -0,0 +1 @@ +require 'arjdbc/mssql' diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/active_record/connection_adapters/mysql2_adapter.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/active_record/connection_adapters/mysql2_adapter.rb new file mode 100644 index 00000000000..51e0c782205 --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -0,0 +1 @@ +require 'arjdbc/mysql' diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/active_record/connection_adapters/mysql_adapter.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/active_record/connection_adapters/mysql_adapter.rb new file mode 100644 index 00000000000..51e0c782205 --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/active_record/connection_adapters/mysql_adapter.rb @@ -0,0 +1 @@ +require 'arjdbc/mysql' diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/active_record/connection_adapters/oracle_adapter.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/active_record/connection_adapters/oracle_adapter.rb new file mode 100644 index 00000000000..3f14e5ac0a1 --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/active_record/connection_adapters/oracle_adapter.rb @@ -0,0 +1 @@ +require 'arjdbc/oracle' diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/active_record/connection_adapters/postgresql_adapter.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/active_record/connection_adapters/postgresql_adapter.rb new file mode 100644 index 00000000000..1872e0b9916 --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -0,0 +1 @@ +require 'arjdbc/postgresql' diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/active_record/connection_adapters/sqlite3_adapter.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/active_record/connection_adapters/sqlite3_adapter.rb new file mode 100644 index 00000000000..6a6cac7fa73 --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -0,0 +1 @@ +require 'arjdbc/sqlite3' diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/activerecord-jdbc-adapter.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/activerecord-jdbc-adapter.rb new file mode 100644 index 00000000000..7c7a33b632a --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/activerecord-jdbc-adapter.rb @@ -0,0 +1,8 @@ +require 'arjdbc' +if ActiveRecord::VERSION::MAJOR >= 3 + begin + require 'arjdbc/jdbc/railtie' + rescue LoadError + # Assume we don't have railties in this version of AR + end +end diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arel/engines/sql/compilers/db2_compiler.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arel/engines/sql/compilers/db2_compiler.rb new file mode 100644 index 00000000000..3766e48352d --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arel/engines/sql/compilers/db2_compiler.rb @@ -0,0 +1,9 @@ +require 'arel/engines/sql/compilers/ibm_db_compiler' + +module Arel + module SqlCompiler + class DB2Compiler < IBM_DBCompiler + end + end +end + diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arel/engines/sql/compilers/derby_compiler.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arel/engines/sql/compilers/derby_compiler.rb new file mode 100644 index 00000000000..e88fb166b68 --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arel/engines/sql/compilers/derby_compiler.rb @@ -0,0 +1,6 @@ +module Arel + module SqlCompiler + class DerbyCompiler < GenericCompiler + end + end +end diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arel/engines/sql/compilers/h2_compiler.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arel/engines/sql/compilers/h2_compiler.rb new file mode 100644 index 00000000000..7e37bb49689 --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arel/engines/sql/compilers/h2_compiler.rb @@ -0,0 +1,6 @@ +module Arel + module SqlCompiler + class H2Compiler < GenericCompiler + end + end +end diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arel/engines/sql/compilers/hsqldb_compiler.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arel/engines/sql/compilers/hsqldb_compiler.rb new file mode 100644 index 00000000000..c6ca111b5c3 --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arel/engines/sql/compilers/hsqldb_compiler.rb @@ -0,0 +1,15 @@ +module Arel + module SqlCompiler + class HsqldbCompiler < GenericCompiler + def select_sql + # HSQLDB needs to add LIMIT in right after SELECT + query = super + offset = relation.skipped + limit = relation.taken + @engine.connection.add_limit_offset!(query, :limit => limit, + :offset => offset) if offset || limit + query + end + end + end +end diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arel/engines/sql/compilers/jdbc_compiler.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arel/engines/sql/compilers/jdbc_compiler.rb new file mode 100644 index 00000000000..5ac6fa55309 --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arel/engines/sql/compilers/jdbc_compiler.rb @@ -0,0 +1,6 @@ +module Arel + module SqlCompiler + class JDBCCompiler < GenericCompiler + end + end +end diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arel/engines/sql/compilers/mssql_compiler.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arel/engines/sql/compilers/mssql_compiler.rb new file mode 100644 index 00000000000..c54bba77ee1 --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arel/engines/sql/compilers/mssql_compiler.rb @@ -0,0 +1,46 @@ +module Arel + module SqlCompiler + class MsSQLCompiler < GenericCompiler + def select_sql + projections = @relation.projections + offset = relation.skipped + limit = relation.taken + if Count === projections.first && projections.size == 1 && + (relation.taken.present? || relation.wheres.present?) && relation.joins(self).blank? + subquery = [ + "SELECT * FROM #{relation.from_clauses}", build_clauses + ].join ' ' + @engine.connection.add_limit_offset!(subquery, :limit => limit, :offset => offset) if offset || limit + query = "SELECT COUNT(*) AS count_id FROM (#{subquery}) AS subquery" + else + query = [ + "SELECT #{relation.select_clauses.join(', ')}", + "FROM #{relation.from_clauses}", + build_clauses + ].compact.join ' ' + @engine.connection.add_limit_offset!(query, :limit => limit, :offset => offset) if offset || limit + end + query + end + + def build_clauses + joins = relation.joins(self) + wheres = relation.where_clauses + groups = relation.group_clauses + havings = relation.having_clauses + orders = relation.order_clauses + + clauses = [ "", + joins, + ("WHERE #{wheres.join(' AND ')}" unless wheres.empty?), + ("GROUP BY #{groups.join(', ')}" unless groups.empty?), + ("HAVING #{havings.join(' AND ')}" unless havings.empty?), + ("ORDER BY #{orders.join(', ')}" unless orders.empty?) + ].compact.join ' ' + + clauses << " #{locked}" unless locked.blank? + clauses unless clauses.blank? + end + end + end +end diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arel/visitors/compat.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arel/visitors/compat.rb new file mode 100644 index 00000000000..c0b4e12d498 --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arel/visitors/compat.rb @@ -0,0 +1,13 @@ +module Arel + module Visitors + module ArJdbcCompat + def limit_for(limit_or_node) + limit_or_node.respond_to?(:expr) ? limit_or_node.expr.to_i : limit_or_node + end + end + + class ToSql + include ArJdbcCompat + end + end +end diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arel/visitors/db2.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arel/visitors/db2.rb new file mode 100644 index 00000000000..926f5be6eec --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arel/visitors/db2.rb @@ -0,0 +1,17 @@ +require 'arel/visitors/compat' + +module Arel + module Visitors + class DB2 < Arel::Visitors::ToSql + def visit_Arel_Nodes_SelectStatement o + add_limit_offset([o.cores.map { |x| visit_Arel_Nodes_SelectCore x }.join, + ("ORDER BY #{o.orders.map { |x| visit x }.join(', ')}" unless o.orders.empty?), + ].compact.join(' '), o) + end + + def add_limit_offset(sql, o) + @connection.replace_limit_offset! sql, limit_for(o.limit), o.offset && o.offset.value + end + end + end +end diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arel/visitors/derby.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arel/visitors/derby.rb new file mode 100644 index 00000000000..4d058bbe1d9 --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arel/visitors/derby.rb @@ -0,0 +1,21 @@ +require 'arel/visitors/compat' + +module Arel + module Visitors + class Derby < Arel::Visitors::ToSql + def visit_Arel_Nodes_SelectStatement o + [ + o.cores.map { |x| visit_Arel_Nodes_SelectCore x }.join, + ("ORDER BY #{o.orders.map { |x| visit x }.join(', ')}" unless o.orders.empty?), + ("FETCH FIRST #{limit_for(o.limit)} ROWS ONLY" if o.limit), + (visit(o.offset) if o.offset), + (visit(o.lock) if o.lock), + ].compact.join ' ' + end + + def visit_Arel_Nodes_Offset o + "OFFSET #{visit o.value} ROWS" + end + end + end +end diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arel/visitors/firebird.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arel/visitors/firebird.rb new file mode 100644 index 00000000000..129edb1d789 --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arel/visitors/firebird.rb @@ -0,0 +1,17 @@ +require 'arel/visitors/compat' + +module Arel + module Visitors + class Firebird < Arel::Visitors::ToSql + def visit_Arel_Nodes_SelectStatement o + [ + o.cores.map { |x| visit_Arel_Nodes_SelectCore x }.join, + ("ORDER BY #{o.orders.map { |x| visit x }.join(', ')}" unless o.orders.empty?), + ("ROWS #{limit_for(o.limit)} " if o.limit), + ("TO #{o.offset} " if o.offset), + ].compact.join ' ' + end + + end + end +end diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arel/visitors/hsqldb.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arel/visitors/hsqldb.rb new file mode 100644 index 00000000000..0f88e1ac148 --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arel/visitors/hsqldb.rb @@ -0,0 +1,26 @@ +require 'arel/visitors/compat' + +module Arel + module Visitors + class HSQLDB < Arel::Visitors::ToSql + def visit_Arel_Nodes_SelectStatement o + [ + limit_offset(o.cores.map { |x| visit_Arel_Nodes_SelectCore x }.join, o), + ("ORDER BY #{o.orders.map { |x| visit x }.join(', ')}" unless o.orders.empty?), + ].compact.join ' ' + end + + def limit_offset sql, o + offset = o.offset || 0 + bef = sql[7..-1] + if limit = o.limit + "SELECT LIMIT #{offset} #{limit_for(limit)} #{bef}" + elsif offset > 0 + "SELECT LIMIT #{offset} 0 #{bef}" + else + sql + end + end + end + end +end diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arel/visitors/sql_server.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arel/visitors/sql_server.rb new file mode 100644 index 00000000000..7562132ab53 --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arel/visitors/sql_server.rb @@ -0,0 +1,44 @@ +require 'arel/visitors/compat' + +module Arel + module Visitors + class SQLServer < Arel::Visitors::ToSql + include ArJdbc::MsSQL::LimitHelpers::SqlServerReplaceLimitOffset + + def select_count? o + sel = o.cores.length == 1 && o.cores.first + projections = sel && sel.projections.length == 1 && sel.projections + projections && Arel::Nodes::Count === projections.first + end + + # Need to mimic the subquery logic in ARel 1.x for select count with limit + # See arel/engines/sql/compilers/mssql_compiler.rb for details + def visit_Arel_Nodes_SelectStatement o + order = "ORDER BY #{o.orders.map { |x| visit x }.join(', ')}" unless o.orders.empty? + if o.limit + if select_count?(o) + subquery = true + sql = o.cores.map do |x| + x = x.dup + x.projections = [Arel::Nodes::SqlLiteral.new("*")] + visit_Arel_Nodes_SelectCore x + end.join + else + sql = o.cores.map { |x| visit_Arel_Nodes_SelectCore x }.join + end + + order ||= "ORDER BY #{@connection.determine_order_clause(sql)}" + replace_limit_offset!(sql, limit_for(o.limit).to_i, o.offset && o.offset.value.to_i, order) + sql = "SELECT COUNT(*) AS count_id FROM (#{sql}) AS subquery" if subquery + else + sql = super + end + sql + end + end + + class SQLServer2000 < SQLServer + include ArJdbc::MsSQL::LimitHelpers::SqlServer2000ReplaceLimitOffset + end + end +end diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-0.9.0.1/lib/jdbc_adapter.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc.rb index 78d7030872b..6ca5d2f1a51 100755..100644 --- a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-0.9.0.1/lib/jdbc_adapter.rb +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc.rb @@ -1,4 +1,4 @@ -if RUBY_PLATFORM =~ /java/ +if defined?(JRUBY_VERSION) begin tried_gem ||= false require 'active_record/version' @@ -16,12 +16,14 @@ if RUBY_PLATFORM =~ /java/ RAILS_CONNECTION_ADAPTERS = %w(jdbc) end if ActiveRecord::VERSION::MAJOR == 1 && ActiveRecord::VERSION::MINOR == 14 - require 'active_record/connection_adapters/jdbc_adapter' + require 'arjdbc/jdbc' end else require 'active_record' - require 'active_record/connection_adapters/jdbc_adapter' + require 'arjdbc/jdbc' end else - warn "ActiveRecord-JDBC is for use with JRuby only" + warn "activerecord-jdbc-adapter is for use with JRuby only" end + +require 'arjdbc/version' diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/db2.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/db2.rb new file mode 100644 index 00000000000..31b44b76de3 --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/db2.rb @@ -0,0 +1,2 @@ +require 'arjdbc/jdbc' +require 'arjdbc/db2/adapter' diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/db2/adapter.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/db2/adapter.rb new file mode 100644 index 00000000000..75144c536f0 --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/db2/adapter.rb @@ -0,0 +1,421 @@ +module ArJdbc + module DB2 + def self.column_selector + [ /(db2|as400)/i, + lambda { |cfg, column| column.extend(::ArJdbc::DB2::Column) } ] + end + + def self.jdbc_connection_class + ::ActiveRecord::ConnectionAdapters::DB2JdbcConnection + end + + module Column + def type_cast(value) + return nil if value.nil? || value =~ /^\s*null\s*$/i + case type + when :string then value + when :integer then defined?(value.to_i) ? value.to_i : (value ? 1 : 0) + when :primary_key then defined?(value.to_i) ? value.to_i : (value ? 1 : 0) + when :float then value.to_f + when :datetime then ArJdbc::DB2::Column.cast_to_date_or_time(value) + when :date then ArJdbc::DB2::Column.cast_to_date_or_time(value) + when :timestamp then ArJdbc::DB2::Column.cast_to_time(value) + when :time then ArJdbc::DB2::Column.cast_to_time(value) + # TODO AS400 stores binary strings in EBCDIC (CCSID 65535), need to convert back to ASCII + else + super + end + end + + def type_cast_code(var_name) + case type + when :datetime then "ArJdbc::DB2::Column.cast_to_date_or_time(#{var_name})" + when :date then "ArJdbc::DB2::Column.cast_to_date_or_time(#{var_name})" + when :timestamp then "ArJdbc::DB2::Column.cast_to_time(#{var_name})" + when :time then "ArJdbc::DB2::Column.cast_to_time(#{var_name})" + else + super + end + end + + def self.cast_to_date_or_time(value) + return value if value.is_a? Date + return nil if value.blank? + guess_date_or_time((value.is_a? Time) ? value : cast_to_time(value)) + end + + def self.cast_to_time(value) + return value if value.is_a? Time + # AS400 returns a 2 digit year, LUW returns a 4 digit year, so comp = true to help out AS400 + time_array = ParseDate.parsedate(value, true) + time_array[0] ||= 2000; time_array[1] ||= 1; time_array[2] ||= 1; + Time.send(ActiveRecord::Base.default_timezone, *time_array) rescue nil + end + + def self.guess_date_or_time(value) + (value.hour == 0 and value.min == 0 and value.sec == 0) ? + Date.new(value.year, value.month, value.day) : value + end + + private + # <b>DEPRECATED:</b> SMALLINT is now used for boolean field types. Please + # convert your tables using DECIMAL(5) for boolean values to SMALLINT instead. + def use_decimal5_for_boolean + warn "[DEPRECATION] using DECIMAL(5) for boolean is deprecated. Convert your columns to SMALLINT instead." + :boolean + end + + # http://publib.boulder.ibm.com/infocenter/db2luw/v9r7/topic/com.ibm.db2.luw.apdv.java.doc/doc/rjvjdata.html + def simplified_type(field_type) + case field_type + # old jdbc_db2.rb used decimal(5,0) as boolean + when /^smallint/i then :boolean + when /^decimal\(5\)$/i then use_decimal5_for_boolean + when /^real/i then :float + when /^timestamp/i then :datetime + else + super + end + end + + # Post process default value from JDBC into a Rails-friendly format (columns{-internal}) + def default_value(value) + # IBM i (AS400) will return an empty string instead of null for no default + return nil if value.blank? + + # string defaults are surrounded by single quotes + return $1 if value =~ /^'(.*)'$/ + + value + end + end + + def _execute(sql, name = nil) + if ActiveRecord::ConnectionAdapters::JdbcConnection::select?(sql) + @connection.execute_query(sql) + elsif ActiveRecord::ConnectionAdapters::JdbcConnection::insert?(sql) + (@connection.execute_insert(sql) or last_insert_id(sql)).to_i + else + @connection.execute_update(sql) + end + end + + # holy moly batman! all this to tell AS400 "yes i am sure" + def execute_and_auto_confirm(sql) + begin + @connection.execute_update "call qsys.qcmdexc('QSYS/CHGJOB INQMSGRPY(*SYSRPYL)',0000000031.00000)" + @connection.execute_update "call qsys.qcmdexc('ADDRPYLE SEQNBR(9876) MSGID(CPA32B2) RPY(''I'')',0000000045.00000)" + rescue Exception => e + raise "Could not call CHGJOB INQMSGRPY(*SYSRPYL) and ADDRPYLE SEQNBR(9876) MSGID(CPA32B2) RPY('I').\n" + + "Do you have authority to do this?\n\n" + e.to_s + end + + r = execute sql + + begin + @connection.execute_update "call qsys.qcmdexc('QSYS/CHGJOB INQMSGRPY(*DFT)',0000000027.00000)" + @connection.execute_update "call qsys.qcmdexc('RMVRPYLE SEQNBR(9876)',0000000021.00000)" + rescue Exception => e + raise "Could not call CHGJOB INQMSGRPY(*DFT) and RMVRPYLE SEQNBR(9876).\n" + + "Do you have authority to do this?\n\n" + e.to_s + end + r + end + + def last_insert_id(sql) + table_name = sql.split(/\s/)[2] + result = select(ActiveRecord::Base.send(:sanitize_sql, + %[select IDENTITY_VAL_LOCAL() as last_insert_id from #{table_name}], + nil)) + result.last['last_insert_id'] + end + + def modify_types(tp) + tp[:primary_key] = 'int not null generated by default as identity (start with 1) primary key' + tp[:string][:limit] = 255 + tp[:integer][:limit] = nil + tp[:boolean] = {:name => "smallint"} + tp + end + + def type_to_sql(type, limit = nil, precision = nil, scale = nil) + limit = nil if type.to_sym == :integer + super(type, limit, precision, scale) + end + + def adapter_name + 'DB2' + end + + def arel2_visitors + require 'arel/visitors/db2' + {'db2' => ::Arel::Visitors::DB2, 'as400' => ::Arel::Visitors::DB2} + end + + def add_limit_offset!(sql, options) + replace_limit_offset!(sql, options[:limit], options[:offset]) + end + + def replace_limit_offset!(sql, limit, offset) + if limit + limit = limit.to_i + if !offset + if limit == 1 + sql << " FETCH FIRST ROW ONLY" + else + sql << " FETCH FIRST #{limit} ROWS ONLY" + end + else + offset = offset.to_i + sql.gsub!(/SELECT/i, 'SELECT B.* FROM (SELECT A.*, row_number() over () AS internal$rownum FROM (SELECT') + sql << ") A ) B WHERE B.internal$rownum > #{offset} AND B.internal$rownum <= #{limit + offset}" + end + end + sql + end + + def pk_and_sequence_for(table) + # In JDBC/DB2 side, only upcase names of table and column are handled. + keys = super(table.upcase) + if keys && keys[0] + # In ActiveRecord side, only downcase names of table and column are handled. + keys[0] = keys[0].downcase + end + keys + end + + def quote_column_name(column_name) + column_name + end + + def quote(value, column = nil) # :nodoc: + if column && column.respond_to?(:primary) && column.primary && column.klass != String + return value.to_i.to_s + end + if column && (column.type == :decimal || column.type == :integer) && value + return value.to_s + end + case value + when String + if column && column.type == :binary + "BLOB('#{quote_string(value)}')" + else + "'#{quote_string(value)}'" + end + else super + end + end + + def quote_string(string) + string.gsub(/'/, "''") # ' (for ruby-mode) + end + + def quoted_true + '1' + end + + def quoted_false + '0' + end + + def reorg_table(table_name) + unless as400? + @connection.execute_update "call sysproc.admin_cmd ('REORG TABLE #{table_name}')" + end + end + + def recreate_database(name) + tables.each {|table| drop_table("#{db2_schema}.#{table}")} + end + + def remove_index(table_name, options = { }) + execute "DROP INDEX #{quote_column_name(index_name(table_name, options))}" + end + + # http://publib.boulder.ibm.com/infocenter/db2luw/v9r7/topic/com.ibm.db2.luw.admin.dbobj.doc/doc/t0020130.html + # ...not supported on IBM i, so we raise in this case + def rename_column(table_name, column_name, new_column_name) #:nodoc: + if as400? + raise NotImplementedError, "rename_column is not supported on IBM i" + else + execute "ALTER TABLE #{table_name} RENAME COLUMN #{column_name} TO #{new_column_name}" + reorg_table(table_name) + end + end + + def change_column_null(table_name, column_name, null) + if null + execute_and_auto_confirm "ALTER TABLE #{table_name} ALTER COLUMN #{column_name} DROP NOT NULL" + else + execute_and_auto_confirm "ALTER TABLE #{table_name} ALTER COLUMN #{column_name} SET NOT NULL" + end + reorg_table(table_name) + end + + def change_column_default(table_name, column_name, default) + if default.nil? + execute_and_auto_confirm "ALTER TABLE #{table_name} ALTER COLUMN #{column_name} DROP DEFAULT" + else + execute_and_auto_confirm "ALTER TABLE #{table_name} ALTER COLUMN #{column_name} SET WITH DEFAULT #{quote(default)}" + end + reorg_table(table_name) + end + + def change_column(table_name, column_name, type, options = {}) + data_type = type_to_sql(type, options[:limit], options[:precision], options[:scale]) + sql = "ALTER TABLE #{table_name} ALTER COLUMN #{column_name} SET DATA TYPE #{data_type}" + as400? ? execute_and_auto_confirm(sql) : execute(sql) + reorg_table(table_name) + + if options.include?(:default) and options.include?(:null) + # which to run first? + if options[:null] or options[:default].nil? + change_column_null(table_name, column_name, options[:null]) + change_column_default(table_name, column_name, options[:default]) + else + change_column_default(table_name, column_name, options[:default]) + change_column_null(table_name, column_name, options[:null]) + end + elsif options.include?(:default) + change_column_default(table_name, column_name, options[:default]) + elsif options.include?(:null) + change_column_null(table_name, column_name, options[:null]) + end + end + + # http://publib.boulder.ibm.com/infocenter/db2luw/v9r7/topic/com.ibm.db2.luw.admin.dbobj.doc/doc/t0020132.html + def remove_column(table_name, column_name) #:nodoc: + sql = "ALTER TABLE #{table_name} DROP COLUMN #{column_name}" + + as400? ? execute_and_auto_confirm(sql) : execute(sql) + reorg_table(table_name) + end + + # http://publib.boulder.ibm.com/infocenter/db2luw/v9r7/topic/com.ibm.db2.luw.sql.ref.doc/doc/r0000980.html + def rename_table(name, new_name) #:nodoc: + execute "RENAME TABLE #{name} TO #{new_name}" + reorg_table(new_name) + end + + def tables + @connection.tables(nil, db2_schema, nil, ["TABLE"]) + end + + # only record precision and scale for types that can set + # them via CREATE TABLE: + # http://publib.boulder.ibm.com/infocenter/db2luw/v9r7/topic/com.ibm.db2.luw.sql.ref.doc/doc/r0000927.html + HAVE_LIMIT = %w(FLOAT DECFLOAT CHAR VARCHAR CLOB BLOB NCHAR NCLOB DBCLOB GRAPHIC VARGRAPHIC) #TIMESTAMP + HAVE_PRECISION = %w(DECIMAL NUMERIC) + HAVE_SCALE = %w(DECIMAL NUMERIC) + + def columns(table_name, name = nil) + cols = @connection.columns(table_name, name, db2_schema) + + # scrub out sizing info when CREATE TABLE doesn't support it + # but JDBC reports it (doh!) + for col in cols + base_sql_type = col.sql_type.sub(/\(.*/, "").upcase + col.limit = nil unless HAVE_LIMIT.include?(base_sql_type) + col.precision = nil unless HAVE_PRECISION.include?(base_sql_type) + #col.scale = nil unless HAVE_SCALE.include?(base_sql_type) + end + + cols + end + + def jdbc_columns(table_name, name = nil) + columns(table_name, name) + end + + def indexes(table_name, name = nil) + @connection.indexes(table_name, name, db2_schema) + end + + def add_quotes(name) + return name unless name + %Q{"#{name}"} + end + + def strip_quotes(str) + return str unless str + return str unless /^(["']).*\1$/ =~ str + str[1..-2] + end + + def expand_double_quotes(name) + return name unless name && name['"'] + name.gsub(/"/,'""') + end + + def structure_dump #:nodoc: + definition="" + rs = @connection.connection.meta_data.getTables(nil,db2_schema.upcase,nil,["TABLE"].to_java(:string)) + while rs.next + tname = rs.getString(3) + definition << "CREATE TABLE #{tname} (\n" + rs2 = @connection.connection.meta_data.getColumns(nil,db2_schema.upcase,tname,nil) + first_col = true + while rs2.next + col_name = add_quotes(rs2.getString(4)); + default = "" + d1 = rs2.getString(13) + # IBM i (as400 toolbox driver) will return an empty string if there is no default + if @config[:url] =~ /^jdbc:as400:/ + default = !d1.blank? ? " DEFAULT #{d1}" : "" + else + default = d1 ? " DEFAULT #{d1}" : "" + end + + type = rs2.getString(6) + col_precision = rs2.getString(7) + col_scale = rs2.getString(9) + col_size = "" + if HAVE_SCALE.include?(type) and col_scale + col_size = "(#{col_precision},#{col_scale})" + elsif (HAVE_LIMIT + HAVE_PRECISION).include?(type) and col_precision + col_size = "(#{col_precision})" + end + nulling = (rs2.getString(18) == 'NO' ? " NOT NULL" : "") + create_col_string = add_quotes(expand_double_quotes(strip_quotes(col_name))) + + " " + + type + + col_size + + "" + + nulling + + default + if !first_col + create_col_string = ",\n #{create_col_string}" + else + create_col_string = " #{create_col_string}" + end + + definition << create_col_string + + first_col = false + end + definition << ");\n\n" + end + definition + end + + private + def as400? + @config[:url] =~ /^jdbc:as400:/ + end + + def db2_schema + if @config[:schema].blank? + if as400? + # AS400 implementation takes schema from library name (last part of url) + schema = @config[:url].split('/').last.strip + (schema[-1..-1] == ";") ? schema.chop : schema + else + # LUW implementation uses schema name of username by default + @config[:username] or ENV['USER'] + end + else + @config[:schema] + end + end + end +end diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/derby.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/derby.rb new file mode 100644 index 00000000000..e7d557a96e1 --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/derby.rb @@ -0,0 +1,7 @@ +require 'arjdbc/jdbc' +jdbc_require_driver 'jdbc/derby' +require 'arjdbc/derby/connection_methods' +require 'arjdbc/derby/adapter' + + + diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-0.9.0.1/lib/jdbc_adapter/jdbc_derby.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/derby/adapter.rb index 56b31eda896..0f2a25c9f10 100755..100644 --- a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-0.9.0.1/lib/jdbc_adapter/jdbc_derby.rb +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/derby/adapter.rb @@ -1,21 +1,9 @@ -require 'jdbc_adapter/missing_functionality_helper' - -module ::JdbcSpec - module ActiveRecordExtensions - def derby_connection(config) - config[:url] ||= "jdbc:derby:#{config[:database]};create=true" - config[:driver] ||= "org.apache.derby.jdbc.EmbeddedDriver" - embedded_driver(config) - end - end +require 'arjdbc/jdbc/missing_functionality_helper' +module ::ArJdbc module Derby def self.column_selector - [/derby/i, lambda {|cfg,col| col.extend(::JdbcSpec::Derby::Column)}] - end - - def self.adapter_selector - [/derby/i, lambda {|cfg,adapt| adapt.extend(::JdbcSpec::Derby)}] + [/derby/i, lambda {|cfg,col| col.extend(::ArJdbc::Derby::Column)}] end def self.monkey_rails @@ -48,49 +36,73 @@ module ::JdbcSpec end module Column - def value_to_binary(value) - value.scan(/[0-9A-Fa-f]{2}/).collect {|v| v.to_i(16)}.pack("C*") + def simplified_type(field_type) + case field_type + when /smallint/i then :boolean + when /real/i then :float + when /decimal/i then :decimal + else + super + end end - def cast_to_date_or_time(value) - return value if value.is_a? Date - return nil if value.blank? - guess_date_or_time((value.is_a? Time) ? value : cast_to_time(value)) - end + # Post process default value from JDBC into a Rails-friendly format (columns{-internal}) + def default_value(value) + # jdbc returns column default strings with actual single quotes around the value. + return $1 if value =~ /^'(.*)'$/ - def cast_to_time(value) - return value if value.is_a? Time - time_array = ParseDate.parsedate value - time_array[0] ||= 2000; time_array[1] ||= 1; time_array[2] ||= 1; - Time.send(ActiveRecord::Base.default_timezone, *time_array) rescue nil + value end + end - def guess_date_or_time(value) - (value.hour == 0 and value.min == 0 and value.sec == 0) ? - Date.new(value.year, value.month, value.day) : value - end + def adapter_name #:nodoc: + 'Derby' + end - def simplified_type(field_type) - return :boolean if field_type =~ /smallint/i - return :float if field_type =~ /real/i - super - end + def arel2_visitors + require 'arel/visitors/derby' + {'derby' => ::Arel::Visitors::Derby, 'jdbcderby' => ::Arel::Visitors::Derby} + end + + include ArJdbc::MissingFunctionalityHelper + + def index_name_length + 128 end - include JdbcSpec::MissingFunctionalityHelper + # Convert the specified column type to a SQL string. + # In Derby, the following cannot specify a limit: + # - integer + # - boolean (smallint) + # - timestamp + # - date + def type_to_sql(type, limit = nil, precision = nil, scale = nil) #:nodoc: + return super unless [:integer, :boolean, :timestamp, :date].include? type + + native = native_database_types[type.to_s.downcase.to_sym] + native.is_a?(Hash) ? native[:name] : native + end def modify_types(tp) tp[:primary_key] = "int generated by default as identity NOT NULL PRIMARY KEY" - tp[:integer][:limit] = nil tp[:string][:limit] = 256 + tp[:integer][:limit] = nil tp[:boolean] = {:name => "smallint"} + tp[:timestamp][:limit] = nil + tp[:date][:limit] = nil + + # sonar + # New type + tp[:big_integer] = {:name => "bigint"} + # /sonar + tp end # Override default -- fix case where ActiveRecord passes :default => nil, :null => true def add_column_options!(sql, options) options.delete(:default) if options.has_key?(:default) && options[:default].nil? - options.delete(:null) if options.has_key?(:null) && (options[:null].nil? || options[:null] == true) + sql << " DEFAULT #{quote(options.delete(:default))}" if options.has_key?(:default) super end @@ -100,8 +112,8 @@ module ::JdbcSpec # Set the sequence to the max value of the table's column. def reset_sequence!(table, column, sequence = nil) - mpk = select_value("SELECT MAX(#{quote_column_name column}) FROM #{table}") - execute("ALTER TABLE #{table} ALTER COLUMN #{quote_column_name column} RESTART WITH #{mpk.to_i + 1}") + mpk = select_value("SELECT MAX(#{quote_column_name(column)}) FROM #{quote_table_name(table)}") + execute("ALTER TABLE #{quote_table_name(table)} ALTER COLUMN #{quote_column_name(column)} RESTART WITH #{mpk.to_i + 1}") end def reset_pk_sequence!(table, pk = nil, sequence = nil) @@ -113,23 +125,14 @@ module ::JdbcSpec end end - def primary_key(table_name) #:nodoc: - primary_keys(table_name).first - end - def remove_index(table_name, options) #:nodoc: execute "DROP INDEX #{index_name(table_name, options)}" end def rename_table(name, new_name) - execute "RENAME TABLE #{name} TO #{new_name}" + execute "RENAME TABLE #{quote_table_name(name)} TO #{quote_table_name(new_name)}" end - COLUMN_INFO_STMT = "SELECT C.COLUMNNAME, C.REFERENCEID, C.COLUMNNUMBER FROM SYS.SYSCOLUMNS C, SYS.SYSTABLES T WHERE T.TABLEID = '%s' AND T.TABLEID = C.REFERENCEID ORDER BY C.COLUMNNUMBER" - - COLUMN_TYPE_STMT = "SELECT COLUMNDATATYPE, COLUMNDEFAULT FROM SYS.SYSCOLUMNS WHERE REFERENCEID = '%s' AND COLUMNNAME = '%s'" - - AUTO_INC_STMT = "SELECT AUTOINCREMENTSTART, AUTOINCREMENTINC, COLUMNNAME, REFERENCEID, COLUMNDEFAULT FROM SYS.SYSCOLUMNS WHERE REFERENCEID = '%s' AND COLUMNNAME = '%s'" AUTO_INC_STMT2 = "SELECT AUTOINCREMENTSTART, AUTOINCREMENTINC, COLUMNNAME, REFERENCEID, COLUMNDEFAULT FROM SYS.SYSCOLUMNS WHERE REFERENCEID = (SELECT T.TABLEID FROM SYS.SYSTABLES T WHERE T.TABLENAME = '%s') AND COLUMNNAME = '%s'" def add_quotes(name) @@ -148,42 +151,6 @@ module ::JdbcSpec name.gsub(/"/,'""') end - def reinstate_auto_increment(name, refid, coldef) - stmt = AUTO_INC_STMT % [refid, strip_quotes(name)] - data = execute(stmt).first - if data - start = data['autoincrementstart'] - if start - coldef << " GENERATED " << (data['columndefault'].nil? ? "ALWAYS" : "BY DEFAULT ") - coldef << "AS IDENTITY (START WITH " - coldef << start - coldef << ", INCREMENT BY " - coldef << data['autoincrementinc'] - coldef << ")" - return true - end - end - false - end - - def reinstate_auto_increment(name, refid, coldef) - stmt = AUTO_INC_STMT % [refid, strip_quotes(name)] - data = execute(stmt).first - if data - start = data['autoincrementstart'] - if start - coldef << " GENERATED " << (data['columndefault'].nil? ? "ALWAYS" : "BY DEFAULT ") - coldef << "AS IDENTITY (START WITH " - coldef << start - coldef << ", INCREMENT BY " - coldef << data['autoincrementinc'] - coldef << ")" - return true - end - end - false - end - def auto_increment_stmt(tname, cname) stmt = AUTO_INC_STMT2 % [tname, strip_quotes(cname)] data = execute(stmt).first @@ -205,31 +172,44 @@ module ::JdbcSpec def add_column(table_name, column_name, type, options = {}) - if option_not_null = options[:null] == false - option_not_null = options.delete(:null) - end add_column_sql = "ALTER TABLE #{quote_table_name(table_name)} ADD #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}" add_column_options!(add_column_sql, options) execute(add_column_sql) - if option_not_null - alter_column_sql = "ALTER TABLE #{quote_table_name(table_name)} ALTER #{quote_column_name(column_name)} NOT NULL" - end end - # I don't think this method is ever called ??? (stepheneb) - def create_column(name, refid, colno) - stmt = COLUMN_TYPE_STMT % [refid, strip_quotes(name)] - coldef = "" - data = execute(stmt).first - if data - coldef << add_quotes(expand_double_quotes(strip_quotes(name))) - coldef << " " - coldef << data['columndatatype'] - if !reinstate_auto_increment(name, refid, coldef) && data['columndefault'] - coldef << " DEFAULT " << data['columndefault'] + def execute(sql, name = nil) + if sql =~ /\A\s*(UPDATE|INSERT)/i + i = sql =~ /\swhere\s/im + if i + sql[i..-1] = sql[i..-1].gsub(/!=\s*NULL/, 'IS NOT NULL').gsub(/=\sNULL/i, 'IS NULL') end + else + sql.gsub!(/= NULL/i, 'IS NULL') end - coldef + super + end + + # SELECT DISTINCT clause for a given set of columns and a given ORDER BY clause. + # + # Derby requires the ORDER BY columns in the select list for distinct queries, and + # requires that the ORDER BY include the distinct column. + # + # distinct("posts.id", "posts.created_at desc") + # + # Based on distinct method for PostgreSQL Adapter + def distinct(columns, order_by) + return "DISTINCT #{columns}" if order_by.blank? + + # construct a clean list of column names from the ORDER BY clause, removing + # any asc/desc modifiers + order_columns = order_by.split(',').collect { |s| s.split.first } + order_columns.delete_if(&:blank?) + order_columns = order_columns.zip((0...order_columns.size).to_a).map { |s,i| "#{s} AS alias_#{i}" } + + # return a DISTINCT clause that's distinct on the columns we want but includes + # all the required columns for the ORDER BY to work properly + sql = "DISTINCT #{columns}, #{order_columns * ', '}" + sql end SIZEABLE = %w(VARCHAR CLOB BLOB) @@ -276,21 +256,8 @@ module ::JdbcSpec definition end - # Support for removing columns added via derby bug issue: - # https://issues.apache.org/jira/browse/DERBY-1489 - # - # This feature has not made it into a formal release and is not in Java 6. - # If the normal strategy fails we fall back on a strategy by creating a new - # table without the new column and there after moving the data to the new - # def remove_column(table_name, column_name) - begin - execute "ALTER TABLE #{table_name} DROP COLUMN #{column_name} RESTRICT" - rescue - alter_table(table_name) do |definition| - definition.columns.delete(definition[column_name]) - end - end + execute "ALTER TABLE #{quote_table_name(table_name)} DROP COLUMN #{quote_column_name(column_name)} RESTRICT" end # Notes about changing in Derby: @@ -306,23 +273,23 @@ module ::JdbcSpec if options.include?(:null) # This seems to only work with 10.2 of Derby if options.delete(:null) == false - execute "ALTER TABLE #{table_name} ALTER COLUMN #{column_name} NOT NULL" + execute "ALTER TABLE #{quote_table_name(table_name)} ALTER COLUMN #{quote_column_name(column_name)} NOT NULL" else - execute "ALTER TABLE #{table_name} ALTER COLUMN #{column_name} NULL" + execute "ALTER TABLE #{quote_table_name(table_name)} ALTER COLUMN #{quote_column_name(column_name)} NULL" end end # anything left to do? unless options.empty? begin - execute "ALTER TABLE #{table_name} ALTER COLUMN #{column_name} SET DATA TYPE #{type_to_sql(type, options[:limit])}" + execute "ALTER TABLE #{quote_table_name(table_name)} ALTER COLUMN #{quote_column_name(column_name)} SET DATA TYPE #{type_to_sql(type, options[:limit])}" rescue transaction do temp_new_column_name = "#{column_name}_newtype" # 1) ALTER TABLE t ADD COLUMN c1_newtype NEWTYPE; add_column table_name, temp_new_column_name, type, options # 2) UPDATE t SET c1_newtype = c1; - execute "UPDATE #{table_name} SET #{temp_new_column_name} = CAST(#{column_name} AS #{type_to_sql(type, options[:limit])})" + execute "UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(temp_new_column_name)} = CAST(#{quote_column_name(column_name)} AS #{type_to_sql(type, options[:limit])})" # 3) ALTER TABLE t DROP COLUMN c1; remove_column table_name, column_name # 4) ALTER TABLE t RENAME COLUMN c1_newtype to c1; @@ -332,44 +299,30 @@ module ::JdbcSpec end end - # Support for renaming columns: - # https://issues.apache.org/jira/browse/DERBY-1490 - # - # This feature is expect to arrive in version 10.3.0.0: - # http://wiki.apache.org/db-derby/DerbyTenThreeRelease) - # def rename_column(table_name, column_name, new_column_name) #:nodoc: - begin - execute "ALTER TABLE #{table_name} ALTER RENAME COLUMN #{column_name} TO #{new_column_name}" - rescue - alter_table(table_name, :rename => {column_name => new_column_name}) - end + execute "RENAME COLUMN #{quote_table_name(table_name)}.#{quote_column_name(column_name)} TO #{quote_column_name(new_column_name)}" end def primary_keys(table_name) @connection.primary_keys table_name.to_s.upcase end + def columns(table_name, name=nil) + @connection.columns_internal(table_name.to_s, name, derby_schema) + end + + def tables + @connection.tables(nil, derby_schema) + end + def recreate_database(db_name) tables.each do |t| drop_table t end end - # For DDL it appears you can quote "" column names, but in queries (like insert it errors out?) def quote_column_name(name) #:nodoc: - name = name.to_s - if /^references$/i =~ name - %Q{"#{name.upcase}"} - elsif /[A-Z]/ =~ name && /[a-z]/ =~ name - %Q{"#{name}"} - elsif name =~ /\s/ - %Q{"#{name.upcase}"} - elsif name =~ /^[_\d]/ - %Q{"#{name.upcase}"} - else - name - end + %Q{"#{name.to_s.upcase.gsub(/"/, '""')}"} end def quoted_true @@ -379,6 +332,27 @@ module ::JdbcSpec def quoted_false '0' end + + def add_limit_offset!(sql, options) #:nodoc: + if options[:offset] + sql << " OFFSET #{options[:offset]} ROWS" + end + if options[:limit] + #ROWS/ROW and FIRST/NEXT mean the same + sql << " FETCH FIRST #{options[:limit]} ROWS ONLY" + end + end + + private + # Derby appears to define schemas using the username + def derby_schema + if @config.has_key?(:schema) + config[:schema] + else + (@config[:username] && @config[:username].to_s) || '' + end + end end end + diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/derby/connection_methods.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/derby/connection_methods.rb new file mode 100644 index 00000000000..6062791c5ee --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/derby/connection_methods.rb @@ -0,0 +1,18 @@ +module ActiveRecord + class Base + class << self + def derby_connection(config) + config[:url] ||= "jdbc:derby:#{config[:database]};create=true" + config[:driver] ||= "org.apache.derby.jdbc.EmbeddedDriver" + conn = embedded_driver(config) + md = conn.jdbc_connection.meta_data + if md.database_major_version < 10 || (md.database_major_version == 10 && md.database_minor_version < 5) + raise ::ActiveRecord::ConnectionFailed, "Derby adapter requires Derby 10.5 or later" + end + conn + end + + alias_method :jdbcderby_connection, :derby_connection + end + end +end diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/discover.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/discover.rb new file mode 100644 index 00000000000..a07ed04ac7f --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/discover.rb @@ -0,0 +1,92 @@ +# arjdbc/discover.rb: Declare ArJdbc.extension modules in this file +# that loads a custom module and adapter. + +module ::ArJdbc + # Adapters built-in to AR are required up-front so we can override + # the native ones + require 'arjdbc/mysql' + extension :MySQL do |name| + name =~ /mysql/i + end + + require 'arjdbc/postgresql' + extension :PostgreSQL do |name| + name =~ /postgre/i + end + + require 'arjdbc/sqlite3' + extension :SQLite3 do |name| + name =~ /sqlite/i + end + + # Other adapters are lazy-loaded + extension :DB2 do |name, config| + if name =~ /(db2|as400)/i && config[:url] !~ /^jdbc:derby:net:/ + require 'arjdbc/db2' + true + end + end + + extension :Derby do |name| + if name =~ /derby/i + require 'arjdbc/derby' + true + end + end + + extension :FireBird do |name| + if name =~ /firebird/i + require 'arjdbc/firebird' + true + end + end + + extension :H2 do |name| + if name =~ /\.h2\./i + require 'arjdbc/h2' + true + end + end + + extension :HSQLDB do |name| + if name =~ /hsqldb/i + require 'arjdbc/hsqldb' + true + end + end + + extension :Informix do |name| + if name =~ /informix/i + require 'arjdbc/informix' + true + end + end + + extension :Mimer do |name| + if name =~ /mimer/i + require 'arjdbc/mimer' + true + end + end + + extension :MsSQL do |name| + if name =~ /sqlserver|tds|Microsoft SQL/i + require 'arjdbc/mssql' + true + end + end + + extension :Oracle do |name| + if name =~ /oracle/i + require 'arjdbc/oracle' + true + end + end + + extension :Sybase do |name| + if name =~ /sybase|tds/i + require 'arjdbc/sybase' + true + end + end +end diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/firebird.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/firebird.rb new file mode 100644 index 00000000000..8a5e4067dc6 --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/firebird.rb @@ -0,0 +1,2 @@ +require 'arjdbc/jdbc' +require 'arjdbc/firebird/adapter' diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-0.9.0.1/lib/jdbc_adapter/jdbc_firebird.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/firebird/adapter.rb index 69ae7b08c04..4a82ca8224e 100755..100644 --- a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-0.9.0.1/lib/jdbc_adapter/jdbc_firebird.rb +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/firebird/adapter.rb @@ -1,7 +1,31 @@ -module ::JdbcSpec +module ::ArJdbc module FireBird - def self.adapter_selector - [/firebird/i, lambda{|cfg,adapt| adapt.extend(::JdbcSpec::FireBird)}] + + def self.extended(mod) + unless @lob_callback_added + ActiveRecord::Base.class_eval do + def after_save_with_firebird_blob + self.class.columns.select { |c| c.sql_type =~ /blob/i }.each do |c| + value = self[c.name] + value = value.to_yaml if unserializable_attribute?(c.name, c) + next if value.nil? + connection.write_large_object(c.type == :binary, c.name, self.class.table_name, self.class.primary_key, quote_value(id), value) + end + end + end + + ActiveRecord::Base.after_save :after_save_with_firebird_blob + @lob_callback_added = true + end + end + + def adapter_name + 'Firebird' + end + + def arel2_visitors + require 'arel/visitors/firebird' + {'firebird' => ::Arel::Visitors::Firebird, 'firebirdsql' => ::Arel::Visitors::Firebird} end def modify_types(tp) @@ -10,7 +34,7 @@ module ::JdbcSpec tp[:integer][:limit] = nil tp end - + def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) # :nodoc: execute(sql, name) id_value @@ -31,11 +55,11 @@ module ::JdbcSpec def default_sequence_name(table_name, primary_key) # :nodoc: "#{table_name}_seq" end - + def next_sequence_value(sequence_name) select_one("SELECT GEN_ID(#{sequence_name}, 1 ) FROM RDB$DATABASE;")["gen_id"] end - + def create_table(name, options = {}) #:nodoc: super(name, options) execute "CREATE GENERATOR #{name}_seq" @@ -44,7 +68,7 @@ module ::JdbcSpec def rename_table(name, new_name) #:nodoc: execute "RENAME #{name} TO #{new_name}" execute "UPDATE RDB$GENERATORS SET RDB$GENERATOR_NAME='#{new_name}_seq' WHERE RDB$GENERATOR_NAME='#{name}_seq'" rescue nil - end + end def drop_table(name, options = {}) #:nodoc: super(name) @@ -62,10 +86,13 @@ module ::JdbcSpec def remove_index(table_name, options) #:nodoc: execute "DROP INDEX #{index_name(table_name, options)}" end - + def quote(value, column = nil) # :nodoc: return value.quoted_id if value.respond_to?(:quoted_id) - + + # BLOBs are updated separately by an after_save trigger. + return value.nil? ? "NULL" : "'#{quote_string(value[0..1])}'" if column && [:binary, :text].include?(column.type) + if [Time, DateTime].include?(value.class) "CAST('#{value.strftime("%Y-%m-%d %H:%M:%S")}' AS TIMESTAMP)" else @@ -79,27 +106,27 @@ module ::JdbcSpec def quote_string(string) # :nodoc: string.gsub(/'/, "''") end - + def quote_column_name(column_name) # :nodoc: %Q("#{ar_to_fb_case(column_name)}") end - + def quoted_true # :nodoc: quote(1) end - + def quoted_false # :nodoc: quote(0) end private - + # Maps uppercase Firebird column names to lowercase for ActiveRecord; # mixed-case columns retain their original case. def fb_to_ar_case(column_name) column_name =~ /[[:lower:]]/ ? column_name : column_name.to_s.downcase end - + # Maps lowercase ActiveRecord column names to uppercase for Fierbird; # mixed-case columns retain their original case. def ar_to_fb_case(column_name) diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/h2.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/h2.rb new file mode 100644 index 00000000000..f8f67566b62 --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/h2.rb @@ -0,0 +1,4 @@ +require 'arjdbc/jdbc' +jdbc_require_driver 'jdbc/h2' +require 'arjdbc/h2/connection_methods' +require 'arjdbc/h2/adapter' diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/h2/adapter.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/h2/adapter.rb new file mode 100644 index 00000000000..f61fcab85ed --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/h2/adapter.rb @@ -0,0 +1,36 @@ +require 'arjdbc/hsqldb/adapter' + +module ArJdbc + module H2 + include HSQLDB + + def self.jdbc_connection_class + ::ActiveRecord::ConnectionAdapters::H2JdbcConnection + end + + def adapter_name #:nodoc: + 'H2' + end + + def arel2_visitors + super.merge 'h2' => ::Arel::Visitors::HSQLDB, 'jdbch2' => ::Arel::Visitors::HSQLDB + end + + def h2_adapter + true + end + + def tables + @connection.tables(nil, h2_schema) + end + + def columns(table_name, name=nil) + @connection.columns_internal(table_name.to_s, name, h2_schema) + end + + private + def h2_schema + @config[:schema] || '' + end + end +end diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/h2/connection_methods.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/h2/connection_methods.rb new file mode 100644 index 00000000000..dc373083a76 --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/h2/connection_methods.rb @@ -0,0 +1,12 @@ +module ActiveRecord + class Base + class << self + def h2_connection(config) + config[:url] ||= "jdbc:h2:#{config[:database]}" + config[:driver] ||= "org.h2.Driver" + embedded_driver(config) + end + alias_method :jdbch2_connection, :h2_connection + end + end +end diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/hsqldb.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/hsqldb.rb new file mode 100644 index 00000000000..6780588514b --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/hsqldb.rb @@ -0,0 +1,4 @@ +require 'arjdbc/jdbc' +jdbc_require_driver 'jdbc/hsqldb' +require 'arjdbc/hsqldb/connection_methods' +require 'arjdbc/hsqldb/adapter' diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/hsqldb/adapter.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/hsqldb/adapter.rb new file mode 100644 index 00000000000..5938b7f985c --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/hsqldb/adapter.rb @@ -0,0 +1,182 @@ +module ::ArJdbc + module HSQLDB + def self.column_selector + [/hsqldb|\.h2\./i, lambda {|cfg,col| col.extend(::ArJdbc::HSQLDB::Column)}] + end + + module Column + private + def simplified_type(field_type) + case field_type + when /longvarchar/i then :text + when /tinyint/i then :boolean + when /real/i then :float + when /decimal/i then :decimal + else + super + end + end + + # Override of ActiveRecord::ConnectionAdapters::Column + def extract_limit(sql_type) + # HSQLDB appears to return "LONGVARCHAR(0)" for :text columns, which + # for AR purposes should be interpreted as "no limit" + return nil if sql_type =~ /\(0\)/ + super + end + + # Post process default value from JDBC into a Rails-friendly format (columns{-internal}) + def default_value(value) + # jdbc returns column default strings with actual single quotes around the value. + return $1 if value =~ /^'(.*)'$/ + + value + end + end + + def adapter_name #:nodoc: + 'Hsqldb' + end + + def arel2_visitors + require 'arel/visitors/hsqldb' + {'hsqldb' => ::Arel::Visitors::HSQLDB, 'jdbchsqldb' => ::Arel::Visitors::HSQLDB} + end + + def modify_types(tp) + tp[:primary_key] = "INTEGER GENERATED BY DEFAULT AS IDENTITY(START WITH 0) PRIMARY KEY" + tp[:integer][:limit] = nil + tp[:boolean][:limit] = nil + # set text and float limits so we don't see odd scales tacked on + # in migrations + tp[:boolean] = { :name => "tinyint" } + tp[:text][:limit] = nil + tp[:float][:limit] = 17 if defined?(::Jdbc::H2) + tp[:string][:limit] = 255 + tp[:datetime] = { :name => "DATETIME" } + tp[:timestamp] = { :name => "DATETIME" } + tp[:time] = { :name => "TIME" } + tp[:date] = { :name => "DATE" } + tp + end + + def quote(value, column = nil) # :nodoc: + return value.quoted_id if value.respond_to?(:quoted_id) + + case value + when String + if respond_to?(:h2_adapter) && value.empty? + "''" + elsif column && column.type == :binary + "'#{value.unpack("H*")}'" + elsif column && (column.type == :integer || + column.respond_to?(:primary) && column.primary && column.klass != String) + value.to_i.to_s + else + "'#{quote_string(value)}'" + end + else + super + end + end + + def quote_column_name(name) #:nodoc: + name = name.to_s + if name =~ /[-]/ + %Q{"#{name.upcase}"} + else + name + end + end + + def quote_string(str) + str.gsub(/'/, "''") + end + + def quoted_true + '1' + end + + def quoted_false + '0' + end + + def add_column(table_name, column_name, type, options = {}) + add_column_sql = "ALTER TABLE #{quote_table_name(table_name)} ADD #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}" + add_column_options!(add_column_sql, options) + execute(add_column_sql) + end + + def change_column(table_name, column_name, type, options = {}) #:nodoc: + execute "ALTER TABLE #{table_name} ALTER COLUMN #{column_name} #{type_to_sql(type, options[:limit])}" + end + + def change_column_default(table_name, column_name, default) #:nodoc: + execute "ALTER TABLE #{table_name} ALTER COLUMN #{column_name} SET DEFAULT #{quote(default)}" + end + + def rename_column(table_name, column_name, new_column_name) #:nodoc: + execute "ALTER TABLE #{table_name} ALTER COLUMN #{column_name} RENAME TO #{new_column_name}" + end + + # Maps logical Rails types to MySQL-specific data types. + def type_to_sql(type, limit = nil, precision = nil, scale = nil) + return super if defined?(::Jdbc::H2) || type.to_s != 'integer' || limit == nil + + type + end + + def rename_table(name, new_name) + execute "ALTER TABLE #{name} RENAME TO #{new_name}" + end + + def last_insert_id + Integer(select_value("CALL IDENTITY()")) + end + + def _execute(sql, name = nil) + result = super + ActiveRecord::ConnectionAdapters::JdbcConnection::insert?(sql) ? last_insert_id : result + end + + def add_limit_offset!(sql, options) #:nodoc: + if sql =~ /^select/i + offset = options[:offset] || 0 + bef = sql[7..-1] + if limit = options[:limit] + sql.replace "SELECT LIMIT #{offset} #{limit} #{bef}" + elsif offset > 0 + sql.replace "SELECT LIMIT #{offset} 0 #{bef}" + end + end + end + + # override to filter out system tables that otherwise end + # up in db/schema.rb during migrations. JdbcConnection#tables + # now takes an optional block filter so we can screen out + # rows corresponding to system tables. HSQLDB names its + # system tables SYSTEM.*, but H2 seems to name them without + # any kind of convention + def tables + @connection.tables.select {|row| row.to_s !~ /^system_/i } + end + + def remove_index(table_name, options = {}) + execute "DROP INDEX #{quote_column_name(index_name(table_name, options))}" + end + + def recreate_database(name) + drop_database(name) + end + + # do nothing since database gets created upon connection. However + # this method gets called by rails db rake tasks so now we're + # avoiding method_missing error + def create_database(name) + end + + def drop_database(name) + execute("DROP ALL OBJECTS") + end + end +end diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/hsqldb/connection_methods.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/hsqldb/connection_methods.rb new file mode 100644 index 00000000000..51f0720e850 --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/hsqldb/connection_methods.rb @@ -0,0 +1,14 @@ +module ActiveRecord + class Base + class << self + def hsqldb_connection(config) + require "arjdbc/hsqldb" + config[:url] ||= "jdbc:hsqldb:#{config[:database]}" + config[:driver] ||= "org.hsqldb.jdbcDriver" + embedded_driver(config) + end + + alias_method :jdbchsqldb_connection, :hsqldb_connection + end + end +end diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/informix.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/informix.rb new file mode 100644 index 00000000000..c418ba2662e --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/informix.rb @@ -0,0 +1,3 @@ +require 'arjdbc/jdbc' +require 'arjdbc/informix/connection_methods' +require 'arjdbc/informix/adapter' diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-0.9.0.1/lib/jdbc_adapter/jdbc_informix.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/informix/adapter.rb index c4fe6f25871..55d10efef61 100755..100644 --- a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-0.9.0.1/lib/jdbc_adapter/jdbc_informix.rb +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/informix/adapter.rb @@ -4,7 +4,7 @@ module ::ActiveRecord private def write_lobs - if connection.is_a?(JdbcSpec::Informix) + if connection.is_a?(ArJdbc::Informix) self.class.columns.each do |c| if [:text, :binary].include? c.type value = self[c.name] @@ -25,16 +25,7 @@ module ::ActiveRecord end end -module ::JdbcSpec - module ActiveRecordExtensions - def informix_connection(config) - config[:port] ||= 9088 - config[:url] ||= "jdbc:informix-sqli://#{config[:host]}:#{config[:port]}/#{config[:database]}:INFORMIXSERVER=#{config[:servername]}" - config[:driver] = 'com.informix.jdbc.IfxDriver' - jdbc_connection(config) - end - end - +module ::ArJdbc module Informix def self.extended(base) @@db_major_version = base.select_one("SELECT dbinfo('version', 'major') version FROM systables WHERE tabid = 1")['version'].to_i @@ -42,12 +33,11 @@ module ::JdbcSpec def self.column_selector [ /informix/i, - lambda { |cfg, column| column.extend(::JdbcSpec::Informix::Column) } ] + lambda { |cfg, column| column.extend(::ArJdbc::Informix::Column) } ] end - def self.adapter_selector - [ /informix/i, - lambda { |cfg, adapter| adapter.extend(::JdbcSpec::Informix) } ] + def self.jdbc_connection_class + ::ActiveRecord::ConnectionAdapters::InformixJdbcConnection end module Column @@ -124,12 +114,12 @@ module ::JdbcSpec super(name, options) execute("CREATE SEQUENCE #{name}_seq") end - + def rename_table(name, new_name) execute("RENAME TABLE #{name} TO #{new_name}") execute("RENAME SEQUENCE #{name}_seq TO #{new_name}_seq") end - + def drop_table(name) super(name) execute("DROP SEQUENCE #{name}_seq") @@ -145,4 +135,4 @@ module ::JdbcSpec execute(sql.gsub(/(!=|<>)\s*null/i, "IS NOT NULL").gsub(/=\s*null/i, "IS NULL"), name) end end # module Informix -end # module ::JdbcSpec +end # module ::ArJdbc diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/informix/connection_methods.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/informix/connection_methods.rb new file mode 100644 index 00000000000..aef28ff5923 --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/informix/connection_methods.rb @@ -0,0 +1,10 @@ +class ActiveRecord::Base + class << self + def informix_connection(config) + config[:port] ||= 9088 + config[:url] ||= "jdbc:informix-sqli://#{config[:host]}:#{config[:port]}/#{config[:database]}:INFORMIXSERVER=#{config[:servername]}" + config[:driver] = 'com.informix.jdbc.IfxDriver' + jdbc_connection(config) + end + end +end diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/jdbc.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/jdbc.rb new file mode 100644 index 00000000000..f508d9845c3 --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/jdbc.rb @@ -0,0 +1,2 @@ +require 'arjdbc/jdbc/adapter' +require 'arjdbc/jdbc/discover' diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/jdbc/adapter.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/jdbc/adapter.rb new file mode 100644 index 00000000000..f1e68f4d551 --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/jdbc/adapter.rb @@ -0,0 +1,283 @@ +require 'active_record/version' +require 'active_record/connection_adapters/abstract_adapter' +require 'arjdbc/version' +require 'arjdbc/jdbc/require_driver' +require 'arjdbc/jdbc/connection_methods' +require 'arjdbc/jdbc/compatibility' +require 'arjdbc/jdbc/core_ext' +require 'arjdbc/jdbc/java' +require 'arjdbc/jdbc/type_converter' +require 'arjdbc/jdbc/driver' +require 'arjdbc/jdbc/column' +require 'arjdbc/jdbc/connection' +require 'arjdbc/jdbc/callbacks' +require 'arjdbc/jdbc/extension' +require 'bigdecimal' + +module ActiveRecord + module ConnectionAdapters + class JdbcAdapter < AbstractAdapter + extend ShadowCoreMethods + include CompatibilityMethods if CompatibilityMethods.needed?(self) + include JdbcConnectionPoolCallbacks if JdbcConnectionPoolCallbacks.needed? + + attr_reader :config + + def initialize(connection, logger, config) + @config = config + spec = adapter_spec config + unless connection + connection_class = jdbc_connection_class spec + connection = connection_class.new config + end + super(connection, logger) + extend spec if spec + configure_arel2_visitors(config) + connection.adapter = self + JndiConnectionPoolCallbacks.prepare(self, connection) + end + + def jdbc_connection_class(spec) + connection_class = spec.jdbc_connection_class if spec && spec.respond_to?(:jdbc_connection_class) + connection_class = ::ActiveRecord::ConnectionAdapters::JdbcConnection unless connection_class + connection_class + end + + def jdbc_column_class + ActiveRecord::ConnectionAdapters::JdbcColumn + end + + # Retrieve the raw java.sql.Connection object. + def jdbc_connection + raw_connection.connection + end + + # Locate specialized adapter specification if one exists based on config data + def adapter_spec(config) + 2.times do + dialect = (config[:dialect] || config[:driver]).to_s + ::ArJdbc.constants.map { |name| ::ArJdbc.const_get name }.each do |constant| + if constant.respond_to? :adapter_matcher + spec = constant.adapter_matcher(dialect, config) + return spec if spec + end + end + + # If nothing matches and we're using jndi, try to automatically detect the database. + break unless config[:jndi] and !config[:dialect] + begin + conn = Java::javax.naming.InitialContext.new.lookup(config[:jndi]).getConnection + config[:dialect] = conn.getMetaData.getDatabaseProductName + + # Derby-specific hack + if ::ArJdbc::Derby.adapter_matcher(config[:dialect], config) + # Needed to set the correct database schema name + config[:username] ||= conn.getMetaData.getUserName + end + rescue + conn.close if conn + end + end + nil + end + + def modify_types(tp) + tp + end + + def adapter_name #:nodoc: + 'JDBC' + end + + def arel2_visitors + {} + end + + def configure_arel2_visitors(config) + if defined?(::Arel::Visitors::VISITORS) + visitors = ::Arel::Visitors::VISITORS + visitor = nil + arel2_visitors.each do |k,v| + visitor = v + visitors[k] = v + end + if visitor && config[:adapter] =~ /^(jdbc|jndi)$/ + visitors[config[:adapter]] = visitor + end + end + end + + def is_a?(klass) # :nodoc: + # This is to fake out current_adapter? conditional logic in AR tests + if Class === klass && klass.name =~ /#{adapter_name}Adapter$/i + true + else + super + end + end + + def supports_migrations? + true + end + + def native_database_types #:nodoc: + @connection.native_database_types + end + + def database_name #:nodoc: + @connection.database_name + end + + def native_sql_to_type(tp) + if /^(.*?)\(([0-9]+)\)/ =~ tp + tname = $1 + limit = $2.to_i + ntype = native_database_types + if ntype[:primary_key] == tp + return :primary_key,nil + else + ntype.each do |name,val| + if name == :primary_key + next + end + if val[:name].downcase == tname.downcase && (val[:limit].nil? || val[:limit].to_i == limit) + return name,limit + end + end + end + elsif /^(.*?)/ =~ tp + tname = $1 + ntype = native_database_types + if ntype[:primary_key] == tp + return :primary_key,nil + else + ntype.each do |name,val| + if val[:name].downcase == tname.downcase && val[:limit].nil? + return name,nil + end + end + end + else + return :string,255 + end + return nil,nil + end + + def active? + @connection.active? + end + + def reconnect! + @connection.reconnect! + @connection + end + + def disconnect! + @connection.disconnect! + end + + def execute(sql, name = nil) + if name == :skip_logging + _execute(sql) + else + log(sql, name) { _execute(sql) } + end + end + + # we need to do it this way, to allow Rails stupid tests to always work + # even if we define a new execute method. Instead of mixing in a new + # execute, an _execute should be mixed in. + def _execute(sql, name = nil) + @connection.execute(sql) + end + + def jdbc_insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) + insert_sql(sql, name, pk, id_value, sequence_name) + end + + def jdbc_update(sql, name = nil) #:nodoc: + execute(sql, name) + end + def jdbc_select_all(sql, name = nil) + select(sql, name) + end + + # Allow query caching to work even when we override alias_method_chain'd methods + alias_chained_method :select_all, :query_cache, :jdbc_select_all + alias_chained_method :update, :query_dirty, :jdbc_update + alias_chained_method :insert, :query_dirty, :jdbc_insert + + # Do we need this? Not in AR 3. + def select_one(sql, name = nil) + select(sql, name).first + end + + def select_rows(sql, name = nil) + rows = [] + select(sql, name).each {|row| rows << row.values } + rows + end + + def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) + id = execute(sql, name = nil) + id_value || id + end + + def jdbc_columns(table_name, name = nil) + @connection.columns(table_name.to_s) + end + alias_chained_method :columns, :query_cache, :jdbc_columns + + def tables(name = nil) + @connection.tables + end + + def table_exists?(name) + jdbc_columns(name) rescue nil + end + + def indexes(table_name, name = nil, schema_name = nil) + @connection.indexes(table_name, name, schema_name) + end + + def begin_db_transaction + @connection.begin + end + + def commit_db_transaction + @connection.commit + end + + def rollback_db_transaction + @connection.rollback + end + + def write_large_object(*args) + @connection.write_large_object(*args) + end + + def pk_and_sequence_for(table) + key = primary_key(table) + [key, nil] if key + end + + def primary_key(table) + primary_keys(table).first + end + + def primary_keys(table) + @connection.primary_keys(table) + end + + def select(*args) + execute(*args) + end + + def translate_exception(e, message) + puts e.backtrace if $DEBUG || ENV['DEBUG'] + super + end + protected :translate_exception + end + end +end diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/jdbc/adapter_java.jar b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/jdbc/adapter_java.jar Binary files differnew file mode 100644 index 00000000000..a1bd68c58fa --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/jdbc/adapter_java.jar diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/jdbc/callbacks.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/jdbc/callbacks.rb new file mode 100644 index 00000000000..27375c60a68 --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/jdbc/callbacks.rb @@ -0,0 +1,49 @@ +module ActiveRecord + module ConnectionAdapters + module JdbcConnectionPoolCallbacks + def self.included(base) + if base.respond_to?(:set_callback) # Rails 3 callbacks + base.set_callback :checkin, :after, :on_checkin + base.set_callback :checkout, :before, :on_checkout + else + base.checkin :on_checkin + base.checkout :on_checkout + end + end + + def self.needed? + ActiveRecord::Base.respond_to?(:connection_pool) + end + + def on_checkin + # default implementation does nothing + end + + def on_checkout + # default implementation does nothing + end + end + + module JndiConnectionPoolCallbacks + def self.prepare(adapter, conn) + if ActiveRecord::Base.respond_to?(:connection_pool) && conn.jndi_connection? + adapter.extend self + conn.disconnect! # disconnect initial connection in JdbcConnection#initialize + end + end + + def on_checkin + disconnect! + end + + def on_checkout + #sonar + # Why do we try to reconnect ? It opens two connections instead of a single one. + # Commenting the reconnection allows to have a single JDBC connection per HTTP request + # https://jira.codehaus.org/browse/SONAR-2784 + #reconnect! + #/sonar + end + end + end +end diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/jdbc/column.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/jdbc/column.rb new file mode 100644 index 00000000000..d307fcc7312 --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/jdbc/column.rb @@ -0,0 +1,47 @@ +module ActiveRecord + module ConnectionAdapters + class JdbcColumn < Column + attr_writer :limit, :precision + + def initialize(config, name, default, *args) + call_discovered_column_callbacks(config) + super(name,default_value(default),*args) + init_column(name, default, *args) + end + + def init_column(*args) + end + + def default_value(val) + val + end + + def self.column_types + # GH #25: reset the column types if the # of constants changed + # since last call + if ::ArJdbc.constants.size != driver_constants.size + @driver_constants = nil + @column_types = nil + end + @column_types ||= driver_constants.select {|c| + c.respond_to? :column_selector }.map {|c| + c.column_selector }.inject({}) {|h,val| + h[val[0]] = val[1]; h } + end + + def self.driver_constants + @driver_constants ||= ::ArJdbc.constants.map {|c| ::ArJdbc.const_get c } + end + + protected + def call_discovered_column_callbacks(config) + dialect = config[:dialect] || config[:driver] + for reg, func in JdbcColumn.column_types + if reg === dialect.to_s + func.call(config,self) + end + end + end + end + end +end diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/jdbc/compatibility.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/jdbc/compatibility.rb new file mode 100644 index 00000000000..e310a30d92f --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/jdbc/compatibility.rb @@ -0,0 +1,51 @@ +# AR's 2.2 version of this method is sufficient, but we need it for +# older versions +if ActiveRecord::VERSION::MAJOR <= 2 && ActiveRecord::VERSION::MINOR < 2 + module ActiveRecord + module ConnectionAdapters # :nodoc: + module SchemaStatements + # Convert the speficied column type to a SQL string. + def type_to_sql(type, limit = nil, precision = nil, scale = nil) + if native = native_database_types[type] + column_type_sql = (native.is_a?(Hash) ? native[:name] : native).dup + + if type == :decimal # ignore limit, use precision and scale + scale ||= native[:scale] + + if precision ||= native[:precision] + if scale + column_type_sql << "(#{precision},#{scale})" + else + column_type_sql << "(#{precision})" + end + elsif scale + raise ArgumentError, "Error adding decimal column: precision cannot be empty if scale if specified" + end + + elsif limit ||= native.is_a?(Hash) && native[:limit] + column_type_sql << "(#{limit})" + end + + column_type_sql + else + type + end + end + end + end + end +end + +module ActiveRecord + module ConnectionAdapters + module CompatibilityMethods + def self.needed?(base) + !base.instance_methods.include?("quote_table_name") + end + + def quote_table_name(name) + quote_column_name(name) + end + end + end +end diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/jdbc/connection.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/jdbc/connection.rb new file mode 100644 index 00000000000..fec934983a6 --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/jdbc/connection.rb @@ -0,0 +1,128 @@ +module ActiveRecord + module ConnectionAdapters + class JdbcConnection + module ConfigHelper + attr_reader :config + + def config=(config) + @config = config.symbolize_keys + end + + def configure_connection + config[:retry_count] ||= 5 + config[:connection_alive_sql] ||= "select 1" + + # sonar + # ActiveRecord must transfer the responsibility of connection pool to the Sonar, + # even if JNDI datasource is not used. + @jndi_connection = true + # /sonar + @connection = nil + if config[:jndi] + begin + configure_jndi + rescue => e + warn "JNDI data source unavailable: #{e.message}; trying straight JDBC" + configure_jdbc + end + else + configure_jdbc + end + end + + def configure_jndi + jndi = config[:jndi].to_s + ctx = javax.naming.InitialContext.new + ds = ctx.lookup(jndi) + @connection_factory = JdbcConnectionFactory.impl do + ds.connection + end + unless config[:driver] + config[:driver] = connection.meta_data.connection.java_class.name + end + @jndi_connection = true + end + + def configure_url + url = config[:url].to_s + if Hash === config[:options] + options = '' + config[:options].each do |k,v| + options << '&' unless options.empty? + options << "#{k}=#{v}" + end + url = url['?'] ? "#{url}&#{options}" : "#{url}?#{options}" unless options.empty? + config[:url] = url + config[:options] = nil + end + url + end + + def configure_jdbc + # sonar + @connection_factory = JdbcConnectionFactory.impl do + ::Java::OrgSonarServerUi::JRubyFacade.getInstance().getConnection() + end + # /sonar + end + end + + attr_reader :adapter, :connection_factory + + # @native_database_types - setup properly by adapter= versus set_native_database_types. + # This contains type information for the adapter. Individual adapters can make tweaks + # by defined modify_types + # + # @native_types - This is the default type settings sans any modifications by the + # individual adapter. My guess is that if we loaded two adapters of different types + # then this is used as a base to be tweaked by each adapter to create @native_database_types + + def initialize(config) + self.config = config + configure_connection + connection # force the connection to load + set_native_database_types + @stmts = {} + rescue ::ActiveRecord::ActiveRecordError + raise + rescue Exception => e + raise ::ActiveRecord::JDBCError.new("The driver encountered an unknown error: #{e}").tap { |err| + err.errno = 0 + err.sql_exception = e + } + end + + def adapter=(adapter) + @adapter = adapter + @native_database_types = dup_native_types + @adapter.modify_types(@native_database_types) + @adapter.config.replace(config) + end + + # Duplicate all native types into new hash structure so it can be modified + # without destroying original structure. + def dup_native_types + types = {} + @native_types.each_pair do |k, v| + types[k] = v.inject({}) do |memo, kv| + memo[kv.first] = begin kv.last.dup rescue kv.last end + memo + end + end + types + end + private :dup_native_types + + def jndi_connection? + @jndi_connection + end + + def active? + @connection + end + + private + include ConfigHelper + end + end +end diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/jdbc/connection_methods.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/jdbc/connection_methods.rb new file mode 100644 index 00000000000..8305592861e --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/jdbc/connection_methods.rb @@ -0,0 +1,16 @@ +class ActiveRecord::Base + class << self + def jdbc_connection(config) + adapter_class = config[:adapter_class] + adapter_class ||= ::ActiveRecord::ConnectionAdapters::JdbcAdapter + adapter_class.new(nil, logger, config) + end + alias jndi_connection jdbc_connection + + def embedded_driver(config) + config[:username] ||= "sa" + config[:password] ||= "" + jdbc_connection(config) + end + end +end diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/jdbc/core_ext.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/jdbc/core_ext.rb new file mode 100644 index 00000000000..7d9df170906 --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/jdbc/core_ext.rb @@ -0,0 +1,24 @@ +module ActiveRecord # :nodoc: + # Represents exceptions that have propagated up through the JDBC API. + class JDBCError < ActiveRecordError + # The vendor code or error number that came from the database + attr_accessor :errno + + # The full Java SQLException object that was raised + attr_accessor :sql_exception + end + + module ConnectionAdapters # :nodoc: + # Allows properly re-wrapping/re-defining methods that may already + # be alias_method_chain'd. + module ShadowCoreMethods + def alias_chained_method(meth, feature, target) + if instance_methods.include?("#{meth}_without_#{feature}") + alias_method "#{meth}_without_#{feature}".to_sym, target + else + alias_method meth, target if meth != target + end + end + end + end +end diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/jdbc/discover.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/jdbc/discover.rb new file mode 100644 index 00000000000..346cf565957 --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/jdbc/discover.rb @@ -0,0 +1,18 @@ +module ArJdbc + def self.discover_extensions + if defined?(::Gem) && ::Gem.respond_to?(:find_files) + files = ::Gem.find_files('arjdbc/discover') + else + files = $LOAD_PATH.map do |p| + discover = File.join(p, 'arjdbc','discover.rb') + File.exist?(discover) ? discover : nil + end.compact + end + files.each do |f| + puts "Loading #{f}" if $DEBUG + require f + end + end + + discover_extensions +end diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/jdbc/driver.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/jdbc/driver.rb new file mode 100644 index 00000000000..fb8665271b7 --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/jdbc/driver.rb @@ -0,0 +1,35 @@ +module ActiveRecord + module ConnectionAdapters + class JdbcDriver + def initialize(name) + @name = name + @driver = driver_class.new + end + + def driver_class + @driver_class ||= begin + driver_class_const = (@name[0...1].capitalize + @name[1..@name.length]).gsub(/\./, '_') + Jdbc::Mutex.synchronized do + unless Jdbc.const_defined?(driver_class_const) + driver_class_name = @name + Jdbc.module_eval do + java_import(driver_class_name) { driver_class_const } + end + end + end + driver_class = Jdbc.const_get(driver_class_const) + raise "You must specify a driver for your JDBC connection" unless driver_class + driver_class + end + end + + def connection(url, user, pass) + # bypass DriverManager to get around problem with dynamically loaded jdbc drivers + props = java.util.Properties.new + props.setProperty("user", user) + props.setProperty("password", pass) + @driver.connect(url, props) + end + end + end +end diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/jdbc/extension.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/jdbc/extension.rb new file mode 100644 index 00000000000..0e94087d8e8 --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/jdbc/extension.rb @@ -0,0 +1,47 @@ +module ArJdbc + # Defines an AR-JDBC extension. An extension consists of a + # declaration using this method and an ArJdbc::XYZ module that + # contains implementation and overrides for methods in + # ActiveRecord::ConnectionAdapters::AbstractAdapter. When you + # declare your extension, you provide a block that detects when a + # database configured to use the extension is present and loads the + # necessary code for it. AR-JDBC will patch the code into the base + # ActiveRecord::ConnectionAdapters::JdbcAdapter by extending an + # instance of it with your extension module. + # + # +name+ should be a symbol that is the name of a module to be + # defined under the +ArJdbc+ module. + # + # +block+ should be a one- or two-arity block that receives the + # dialect name or driver class name as the first argument, and + # optionally the whole database configuration hash as a second + # argument. + # + # Example: + # + # ArJdbc.extension :Frob do |name| + # if name =~ /frob/i + # # arjdbc/frob.rb should contain the implementation + # require 'arjdbc/frob' + # true + # end + # end + def self.extension(name,&block) + if const_defined?(name) + mod = const_get(name) + else + mod = const_set(name, Module.new) + end + (class << mod; self; end).instance_eval do + unless respond_to?(:adapter_matcher) + define_method :adapter_matcher do |name, config| + if block.arity == 1 + block.call(name) ? mod : false + else + block.call(name, config) ? mod : false + end + end + end + end + end +end diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/jdbc/java.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/jdbc/java.rb new file mode 100644 index 00000000000..1cea22f91b8 --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/jdbc/java.rb @@ -0,0 +1,14 @@ +require 'java' +require 'arjdbc/jdbc/adapter_java' + +module ActiveRecord + module ConnectionAdapters + module Jdbc + Mutex = java.lang.Object.new + DriverManager = java.sql.DriverManager + Types = java.sql.Types + end + + java_import "arjdbc.jdbc.JdbcConnectionFactory" + end +end diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/jdbc/jdbc.rake b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/jdbc/jdbc.rake new file mode 100644 index 00000000000..bd8f8694417 --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/jdbc/jdbc.rake @@ -0,0 +1,131 @@ +def redefine_task(*args, &block) + task_name = Hash === args.first ? args.first.keys[0] : args.first + existing_task = Rake.application.lookup task_name + if existing_task + class << existing_task + public :instance_variable_set + attr_reader :actions + end + existing_task.instance_variable_set "@prerequisites", FileList[] + existing_task.actions.shift + enhancements = existing_task.actions + existing_task.instance_variable_set "@actions", [] + end + redefined_task = task(*args, &block) + enhancements.each {|enhancement| redefined_task.actions << enhancement} +end + +def rails_env + defined?(Rails.env) ? Rails.env : RAILS_ENV +end + +namespace :db do + redefine_task :create => :rails_env do + create_database(ActiveRecord::Base.configurations[rails_env]) + end + task :create => :load_config if Rake.application.lookup(:load_config) + + redefine_task :drop => :environment do + config = ActiveRecord::Base.configurations[rails_env] + begin + db = find_database_name(config) + ActiveRecord::Base.connection.drop_database(db) + rescue + drop_database(config.merge('adapter' => config['adapter'].sub(/^jdbc/, ''))) + end + end + task :drop => :load_config if Rake.application.lookup(:load_config) + + namespace :create do + task :all => :rails_env + end + + namespace :drop do + task :all => :environment + end + + class << self + alias_method :previous_create_database, :create_database + alias_method :previous_drop_database, :drop_database + end + + def find_database_name(config) + db = config['database'] + if config['adapter'] =~ /postgresql/i + config = config.dup + if config['url'] + url = config['url'].dup + db = url[/\/([^\/]*)$/, 1] + if db + url[/\/([^\/]*)$/, 1] = 'postgres' + config['url'] = url + end + else + db = config['database'] + config['database'] = 'postgres' + end + ActiveRecord::Base.establish_connection(config) + else + ActiveRecord::Base.establish_connection(config) + db = ActiveRecord::Base.connection.database_name + end + db + end + + def create_database(config) + begin + ActiveRecord::Base.establish_connection(config) + ActiveRecord::Base.connection + rescue + begin + if url = config['url'] and url =~ /^(.*(?<!\/)\/)(?=\w)/ + url = $1 + end + + ActiveRecord::Base.establish_connection(config.merge({'database' => nil, 'url' => url})) + ActiveRecord::Base.connection.create_database(config['database']) + ActiveRecord::Base.establish_connection(config) + rescue => e + raise e unless config['adapter'] =~ /mysql|postgresql|sqlite/ + previous_create_database(config.merge('adapter' => config['adapter'].sub(/^jdbc/, ''))) + end + end + end + + def drop_database(config) + previous_drop_database(config.merge('adapter' => config['adapter'].sub(/^jdbc/, ''))) + end + + namespace :structure do + redefine_task :dump => :environment do + abcs = ActiveRecord::Base.configurations + ActiveRecord::Base.establish_connection(abcs[rails_env]) + File.open("db/#{rails_env}_structure.sql", "w+") { |f| f << ActiveRecord::Base.connection.structure_dump } + if ActiveRecord::Base.connection.supports_migrations? + File.open("db/#{rails_env}_structure.sql", "a") { |f| f << ActiveRecord::Base.connection.dump_schema_information } + end + end + end + + namespace :test do + redefine_task :clone_structure => [ "db:structure:dump", "db:test:purge" ] do + abcs = ActiveRecord::Base.configurations + abcs['test']['pg_params'] = '?allowEncodingChanges=true' if abcs['test']['adapter'] =~ /postgresql/i + ActiveRecord::Base.establish_connection(abcs["test"]) + ActiveRecord::Base.connection.execute('SET foreign_key_checks = 0') if abcs["test"]["adapter"] =~ /mysql/i + IO.readlines("db/#{rails_env}_structure.sql").join.split(";\n\n").each do |ddl| + begin + ActiveRecord::Base.connection.execute(ddl.chomp(';')) + rescue Exception => ex + puts ex.message + end + end + end + + redefine_task :purge => :environment do + abcs = ActiveRecord::Base.configurations + db = find_database_name(abcs['test']) + ActiveRecord::Base.connection.recreate_database(db) + end + end +end diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/jdbc/missing_functionality_helper.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/jdbc/missing_functionality_helper.rb new file mode 100644 index 00000000000..53df2c0469d --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/jdbc/missing_functionality_helper.rb @@ -0,0 +1,87 @@ +module ArJdbc + module MissingFunctionalityHelper + #Taken from SQLite adapter + + def alter_table(table_name, options = {}) #:nodoc: + table_name = table_name.to_s.downcase + altered_table_name = "altered_#{table_name}" + caller = lambda {|definition| yield definition if block_given?} + + transaction do + # A temporary table might improve performance here, but + # it doesn't seem to maintain indices across the whole move. + move_table(table_name, altered_table_name, + options) + move_table(altered_table_name, table_name, &caller) + end + end + + def move_table(from, to, options = {}, &block) #:nodoc: + copy_table(from, to, options, &block) + drop_table(from) + end + + def copy_table(from, to, options = {}) #:nodoc: + options = options.merge(:id => (!columns(from).detect{|c| c.name == 'id'}.nil? && 'id' == primary_key(from).to_s)) + create_table(to, options) do |definition| + @definition = definition + columns(from).each do |column| + column_name = options[:rename] ? + (options[:rename][column.name] || + options[:rename][column.name.to_sym] || + column.name) : column.name + + @definition.column(column_name, column.type, + :limit => column.limit, :default => column.default, + :null => column.null) + end + @definition.primary_key(primary_key(from)) if primary_key(from) + yield @definition if block_given? + end + + copy_table_indexes(from, to, options[:rename] || {}) + copy_table_contents(from, to, + @definition.columns.map {|column| column.name}, + options[:rename] || {}) + end + + def copy_table_indexes(from, to, rename = {}) #:nodoc: + indexes(from).each do |index| + name = index.name.downcase + if to == "altered_#{from}" + name = "temp_#{name}" + elsif from == "altered_#{to}" + name = name[5..-1] + end + + to_column_names = columns(to).map(&:name) + columns = index.columns.map {|c| rename[c] || c }.select do |column| + to_column_names.include?(column) + end + + unless columns.empty? + # index name can't be the same + opts = { :name => name.gsub(/(_?)(#{from})_/, "\\1#{to}_") } + opts[:unique] = true if index.unique + add_index(to, columns, opts) + end + end + end + + def copy_table_contents(from, to, columns, rename = {}) #:nodoc: + column_mappings = Hash[*columns.map {|name| [name, name]}.flatten] + rename.inject(column_mappings) {|map, a| map[a.last] = a.first; map} + from_columns = columns(from).collect {|col| col.name} + columns = columns.find_all{|col| from_columns.include?(column_mappings[col])} + quoted_columns = columns.map { |col| quote_column_name(col) } * ',' + + quoted_to = quote_table_name(to) + execute("SELECT * FROM #{quote_table_name(from)}").each do |row| + sql = "INSERT INTO #{quoted_to} (#{quoted_columns}) VALUES (" + sql << columns.map {|col| quote row[column_mappings[col]]} * ', ' + sql << ')' + execute sql + end + end + end +end diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/jdbc/quoted_primary_key.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/jdbc/quoted_primary_key.rb new file mode 100644 index 00000000000..b91185eab29 --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/jdbc/quoted_primary_key.rb @@ -0,0 +1,28 @@ +module ArJdbc + module QuotedPrimaryKeyExtension + def self.extended(base) + # Rails 3 method Rails 2 method + meth = [:arel_attributes_values, :attributes_with_quotes].detect do |m| + base.private_instance_methods.include?(m.to_s) + end + pk_hash_key = "self.class.primary_key" + pk_hash_value = '"?"' + if meth == :arel_attributes_values + pk_hash_key = "self.class.arel_table[#{pk_hash_key}]" + pk_hash_value = "Arel::SqlLiteral.new(#{pk_hash_value})" + end + if meth + base.module_eval <<-PK, __FILE__, __LINE__ + alias :#{meth}_pre_pk :#{meth} + def #{meth}(include_primary_key = true, *args) #:nodoc: + aq = #{meth}_pre_pk(include_primary_key, *args) + if connection.is_a?(ArJdbc::Oracle) || connection.is_a?(ArJdbc::Mimer) + aq[#{pk_hash_key}] = #{pk_hash_value} if include_primary_key && aq[#{pk_hash_key}].nil? + end + aq + end + PK + end + end + end +end diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/jdbc/railtie.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/jdbc/railtie.rb new file mode 100644 index 00000000000..26a61191329 --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/jdbc/railtie.rb @@ -0,0 +1,9 @@ +require 'rails/railtie' + +module ::ArJdbc + class Railtie < ::Rails::Railtie + rake_tasks do + load File.expand_path('../rake_tasks.rb', __FILE__) + end + end +end diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-0.9.0.1/lib/jdbc_adapter/rake_tasks.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/jdbc/rake_tasks.rb index 14ee5d34d2f..186b0dbd13c 100755..100644 --- a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-0.9.0.1/lib/jdbc_adapter/rake_tasks.rb +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/jdbc/rake_tasks.rb @@ -1,6 +1,6 @@ if defined?(Rake.application) && Rake.application && ENV["SKIP_AR_JDBC_RAKE_REDEFINES"].nil? jdbc_rakefile = File.dirname(__FILE__) + "/jdbc.rake" - if Rake.application.lookup("environment") + if Rake.application.lookup("db:create") # rails tasks already defined; load the override tasks now load jdbc_rakefile else diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/jdbc/require_driver.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/jdbc/require_driver.rb new file mode 100644 index 00000000000..524b5cdf8ba --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/jdbc/require_driver.rb @@ -0,0 +1,16 @@ +module Kernel + # load a JDBC driver library/gem, failing silently. If failed, trust + # that the driver jar is already present through some other means + def jdbc_require_driver(path, gem_name = nil) + gem_name ||= path.sub('/', '-') + 2.times do + begin + require path + break + rescue LoadError + require 'rubygems' + begin; gem gem_name; rescue LoadError; end + end + end + end +end diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/jdbc/type_converter.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/jdbc/type_converter.rb new file mode 100644 index 00000000000..eca9da0933d --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/jdbc/type_converter.rb @@ -0,0 +1,126 @@ +module ActiveRecord + module ConnectionAdapters + # I want to use JDBC's DatabaseMetaData#getTypeInfo to choose the best native types to + # use for ActiveRecord's Adapter#native_database_types in a database-independent way, + # but apparently a database driver can return multiple types for a given + # java.sql.Types constant. So this type converter uses some heuristics to try to pick + # the best (most common) type to use. It's not great, it would be better to just + # delegate to each database's existin AR adapter's native_database_types method, but I + # wanted to try to do this in a way that didn't pull in all the other adapters as + # dependencies. Suggestions appreciated. + class JdbcTypeConverter + # The basic ActiveRecord types, mapped to an array of procs that are used to #select + # the best type. The procs are used as selectors in order until there is only one + # type left. If all the selectors are applied and there is still more than one + # type, an exception will be raised. + AR_TO_JDBC_TYPES = { + :string => [ lambda {|r| Jdbc::Types::VARCHAR == r['data_type'].to_i}, + lambda {|r| r['type_name'] =~ /^varchar/i}, + lambda {|r| r['type_name'] =~ /^varchar$/i}, + lambda {|r| r['type_name'] =~ /varying/i}], + :text => [ lambda {|r| [Jdbc::Types::LONGVARCHAR, Jdbc::Types::CLOB].include?(r['data_type'].to_i)}, + lambda {|r| r['type_name'] =~ /^text$/i}, # For Informix + lambda {|r| r['type_name'] =~ /sub_type 1$/i}, # For FireBird + lambda {|r| r['type_name'] =~ /^(text|clob)$/i}, + lambda {|r| r['type_name'] =~ /^character large object$/i}, + lambda {|r| r['sql_data_type'] == 2005}], + :integer => [ lambda {|r| Jdbc::Types::INTEGER == r['data_type'].to_i}, + lambda {|r| r['type_name'] =~ /^integer$/i}, + lambda {|r| r['type_name'] =~ /^int4$/i}, + lambda {|r| r['type_name'] =~ /^int$/i}], + :decimal => [ lambda {|r| Jdbc::Types::DECIMAL == r['data_type'].to_i}, + lambda {|r| r['type_name'] =~ /^decimal$/i}, + lambda {|r| r['type_name'] =~ /^numeric$/i}, + lambda {|r| r['type_name'] =~ /^number$/i}, + lambda {|r| r['type_name'] =~ /^real$/i}, + lambda {|r| r['precision'] == '38'}, + lambda {|r| r['data_type'] == '2'}], + :float => [ lambda {|r| [Jdbc::Types::FLOAT,Jdbc::Types::DOUBLE, Jdbc::Types::REAL].include?(r['data_type'].to_i)}, + lambda {|r| r['data_type'].to_i == Jdbc::Types::REAL}, #Prefer REAL to DOUBLE for Postgresql + lambda {|r| r['type_name'] =~ /^float/i}, + lambda {|r| r['type_name'] =~ /^double$/i}, + lambda {|r| r['type_name'] =~ /^real$/i}, + lambda {|r| r['precision'] == '15'}], + :datetime => [ lambda {|r| Jdbc::Types::TIMESTAMP == r['data_type'].to_i}, + lambda {|r| r['type_name'] =~ /^datetime$/i}, + lambda {|r| r['type_name'] =~ /^timestamp$/i}, + lambda {|r| r['type_name'] =~ /^date/i}, + lambda {|r| r['type_name'] =~ /^integer/i}], #Num of milliseconds for SQLite3 JDBC Driver + :timestamp => [ lambda {|r| Jdbc::Types::TIMESTAMP == r['data_type'].to_i}, + lambda {|r| r['type_name'] =~ /^timestamp$/i}, + lambda {|r| r['type_name'] =~ /^datetime/i}, + lambda {|r| r['type_name'] =~ /^date/i}, + lambda {|r| r['type_name'] =~ /^integer/i}], #Num of milliseconds for SQLite3 JDBC Driver + :time => [ lambda {|r| Jdbc::Types::TIME == r['data_type'].to_i}, + lambda {|r| r['type_name'] =~ /^time$/i}, + lambda {|r| r['type_name'] =~ /^datetime/i}, # For Informix + lambda {|r| r['type_name'] =~ /^date/i}, + lambda {|r| r['type_name'] =~ /^integer/i}], #Num of milliseconds for SQLite3 JDBC Driver + :date => [ lambda {|r| Jdbc::Types::DATE == r['data_type'].to_i}, + lambda {|r| r['type_name'] =~ /^date$/i}, + lambda {|r| r['type_name'] =~ /^date/i}, + lambda {|r| r['type_name'] =~ /^integer/i}], #Num of milliseconds for SQLite3 JDBC Driver3 + :binary => [ lambda {|r| [Jdbc::Types::LONGVARBINARY,Jdbc::Types::BINARY,Jdbc::Types::BLOB].include?(r['data_type'].to_i)}, + lambda {|r| r['type_name'] =~ /^blob/i}, + lambda {|r| r['type_name'] =~ /sub_type 0$/i}, # For FireBird + lambda {|r| r['type_name'] =~ /^varbinary$/i}, # We want this sucker for Mimer + lambda {|r| r['type_name'] =~ /^binary$/i}, ], + :boolean => [ lambda {|r| [Jdbc::Types::TINYINT].include?(r['data_type'].to_i)}, + lambda {|r| r['type_name'] =~ /^bool/i}, + lambda {|r| r['data_type'] == '-7'}, + lambda {|r| r['type_name'] =~ /^tinyint$/i}, + lambda {|r| r['type_name'] =~ /^decimal$/i}, + lambda {|r| r['type_name'] =~ /^integer$/i}] + } + + def initialize(types) + @types = types + @types.each {|t| t['type_name'] ||= t['local_type_name']} # Sybase driver seems to want 'local_type_name' + end + + def choose_best_types + type_map = {} + @types.each do |row| + name = row['type_name'].downcase + k = name.to_sym + type_map[k] = { :name => name } + set_limit_to_nonzero_precision(type_map[k], row) + end + + AR_TO_JDBC_TYPES.keys.each do |k| + typerow = choose_type(k) + type_map[k] = { :name => typerow['type_name'].downcase } + case k + when :integer, :string, :decimal + set_limit_to_nonzero_precision(type_map[k], typerow) + when :boolean + type_map[k][:limit] = 1 + end + end + type_map + end + + def choose_type(ar_type) + procs = AR_TO_JDBC_TYPES[ar_type] + types = @types + procs.each do |p| + new_types = types.reject {|r| r["data_type"].to_i == Jdbc::Types::OTHER} + new_types = new_types.select(&p) + new_types = new_types.inject([]) do |typs,t| + typs << t unless typs.detect {|el| el['type_name'] == t['type_name']} + typs + end + return new_types.first if new_types.length == 1 + types = new_types if new_types.length > 0 + end + raise "unable to choose type for #{ar_type} from:\n#{types.collect{|t| t['type_name']}.inspect}" + end + + def set_limit_to_nonzero_precision(map, row) + if row['precision'] && row['precision'].to_i > 0 + map[:limit] = row['precision'].to_i + end + end + end + end +end diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/mimer.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/mimer.rb new file mode 100644 index 00000000000..cc6e6349dd7 --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/mimer.rb @@ -0,0 +1,2 @@ +require 'arjdbc/jdbc' +require 'arjdbc/mimer/adapter' diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-0.9.0.1/lib/jdbc_adapter/jdbc_mimer.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/mimer/adapter.rb index fe03f2d4ff3..f7cd65d60b1 100755..100644 --- a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-0.9.0.1/lib/jdbc_adapter/jdbc_mimer.rb +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/mimer/adapter.rb @@ -1,7 +1,8 @@ -module JdbcSpec +module ArJdbc module Mimer - def self.adapter_selector - [/mimer/i, lambda {|cfg,adapt| adapt.extend(::JdbcSpec::Mimer)}] + def self.extended(mod) + require 'arjdbc/jdbc/quoted_primary_key' + ActiveRecord::Base.extend ArJdbc::QuotedPrimaryKeyExtension end def modify_types(tp) @@ -16,7 +17,7 @@ module JdbcSpec tp[:date] = { :name => "TIMESTAMP" } tp end - + def default_sequence_name(table, column) #:nodoc: "#{table}_seq" end @@ -50,13 +51,13 @@ module JdbcSpec log(sql, name) { @connection.execute_insert sql,pk } else # Assume the sql contains a bind-variable for the id id_value = select_one("SELECT NEXT_VALUE OF #{sequence_name} AS val FROM MIMER.ONEROW")['val'] - log(sql, name) { + log(sql, name) { execute_prepared_insert(sql,id_value) } end id_value end - + def execute_prepared_insert(sql, id) @stmts ||= {} @stmts[sql] ||= @connection.ps(sql) @@ -68,18 +69,25 @@ module JdbcSpec def quote(value, column = nil) #:nodoc: return value.quoted_id if value.respond_to?(:quoted_id) - + if String === value && column && column.type == :binary return "X'#{quote_string(value.unpack("C*").collect {|v| v.to_s(16)}.join)}'" end case value - when String : %Q{'#{quote_string(value)}'} - when NilClass : 'NULL' - when TrueClass : '1' - when FalseClass : '0' - when Numeric : value.to_s - when Date, Time : %Q{TIMESTAMP '#{value.strftime("%Y-%m-%d %H:%M:%S")}'} - else %Q{'#{quote_string(value.to_yaml)}'} + when String + %Q{'#{quote_string(value)}'} + when NilClass + 'NULL' + when TrueClass + '1' + when FalseClass + '0' + when Numeric + value.to_s + when Date, Time + %Q{TIMESTAMP '#{value.strftime("%Y-%m-%d %H:%M:%S")}'} + else + %Q{'#{quote_string(value.to_yaml)}'} end end @@ -95,7 +103,7 @@ module JdbcSpec @limit = options[:limit] @offset = options[:offset] end - + def select_all(sql, name = nil) @offset ||= 0 if !@limit || @limit == -1 @@ -107,7 +115,7 @@ module JdbcSpec ensure @limit = @offset = nil end - + def select_one(sql, name = nil) @offset ||= 0 select(sql, name)[@offset] diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/mssql.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/mssql.rb new file mode 100644 index 00000000000..ae9eb889c11 --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/mssql.rb @@ -0,0 +1,4 @@ +require 'arjdbc/jdbc' +jdbc_require_driver 'jdbc/jtds', 'jdbc-mssql' +require 'arjdbc/mssql/connection_methods' +require 'arjdbc/mssql/adapter' diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/mssql/adapter.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/mssql/adapter.rb new file mode 100644 index 00000000000..6513da01f9d --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/mssql/adapter.rb @@ -0,0 +1,477 @@ +require 'arjdbc/mssql/tsql_helper' +require 'arjdbc/mssql/limit_helpers' + +module ::ArJdbc + module MsSQL + include TSqlMethods + include LimitHelpers + + def self.extended(mod) + unless @lob_callback_added + ActiveRecord::Base.class_eval do + def after_save_with_mssql_lob + self.class.columns.select { |c| c.sql_type =~ /image/i }.each do |c| + value = self[c.name] + value = value.to_yaml if unserializable_attribute?(c.name, c) + next if value.nil? || (value == '') + + connection.write_large_object(c.type == :binary, c.name, self.class.table_name, self.class.primary_key, quote_value(id), value) + end + end + end + + ActiveRecord::Base.after_save :after_save_with_mssql_lob + @lob_callback_added = true + end + mod.add_version_specific_add_limit_offset + end + + def self.column_selector + [/sqlserver|tds|Microsoft SQL/i, lambda {|cfg,col| col.extend(::ArJdbc::MsSQL::Column)}] + end + + def self.jdbc_connection_class + ::ActiveRecord::ConnectionAdapters::MssqlJdbcConnection + end + + def arel2_visitors + require 'arel/visitors/sql_server' + visitor_class = sqlserver_version == "2000" ? ::Arel::Visitors::SQLServer2000 : ::Arel::Visitors::SQLServer + { 'mssql' => visitor_class, 'sqlserver' => visitor_class, 'jdbcmssql' => visitor_class} + end + + def sqlserver_version + @sqlserver_version ||= select_value("select @@version")[/Microsoft SQL Server\s+(\d{4})/, 1] + end + + def add_version_specific_add_limit_offset + if sqlserver_version == "2000" + extend LimitHelpers::SqlServer2000AddLimitOffset + else + extend LimitHelpers::SqlServerAddLimitOffset + end + end + + def modify_types(tp) #:nodoc: + super(tp) + tp[:string] = {:name => "NVARCHAR", :limit => 255} + if sqlserver_version == "2000" + tp[:text] = {:name => "NTEXT"} + else + tp[:text] = {:name => "NVARCHAR(MAX)"} + end + + # sonar + tp[:big_integer] = { :name => "bigint"} + # /sonar + + tp + end + + def type_to_sql(type, limit = nil, precision = nil, scale = nil) #:nodoc: + # MSSQL's NVARCHAR(n | max) column supports either a number between 1 and + # 4000, or the word "MAX", which corresponds to 2**30-1 UCS-2 characters. + # + # It does not accept NVARCHAR(1073741823) here, so we have to change it + # to NVARCHAR(MAX), even though they are logically equivalent. + # + # MSSQL Server 2000 is skipped here because I don't know how it will behave. + # + # See: http://msdn.microsoft.com/en-us/library/ms186939.aspx + if type.to_s == 'string' and limit == 1073741823 and sqlserver_version != "2000" + 'NVARCHAR(MAX)' + elsif %w( boolean date datetime ).include?(type.to_s) + super(type) # cannot specify limit/precision/scale with these types + else + super + end + end + + module Column + attr_accessor :identity, :is_special + + def simplified_type(field_type) + case field_type + when /int|bigint|smallint|tinyint/i then :integer + when /numeric/i then (@scale.nil? || @scale == 0) ? :integer : :decimal + when /float|double|decimal|money|real|smallmoney/i then :decimal + when /datetime|smalldatetime/i then :datetime + when /timestamp/i then :timestamp + when /time/i then :time + when /date/i then :date + when /text|ntext|xml/i then :text + when /binary|image|varbinary/i then :binary + when /char|nchar|nvarchar|string|varchar/i then (@limit == 1073741823 ? (@limit = nil; :text) : :string) + when /bit/i then :boolean + when /uniqueidentifier/i then :string + end + end + + def default_value(value) + return $1 if value =~ /^\(N?'(.*)'\)$/ + value + end + + def type_cast(value) + return nil if value.nil? || value == "(null)" || value == "(NULL)" + case type + when :integer then value.to_i rescue unquote(value).to_i rescue value ? 1 : 0 + when :primary_key then value == true || value == false ? value == true ? 1 : 0 : value.to_i + when :decimal then self.class.value_to_decimal(unquote(value)) + when :datetime then cast_to_datetime(value) + when :timestamp then cast_to_time(value) + when :time then cast_to_time(value) + when :date then cast_to_date(value) + when :boolean then value == true or (value =~ /^t(rue)?$/i) == 0 or unquote(value)=="1" + when :binary then unquote value + else value + end + end + + def extract_limit(sql_type) + case sql_type + when /text|ntext|xml|binary|image|varbinary|bit/ + nil + else + super + end + end + + def is_utf8? + sql_type =~ /nvarchar|ntext|nchar/i + end + + def unquote(value) + value.to_s.sub(/\A\([\(\']?/, "").sub(/[\'\)]?\)\Z/, "") + end + + def cast_to_time(value) + return value if value.is_a?(Time) + time_array = ParseDate.parsedate(value) + return nil if !time_array.any? + time_array[0] ||= 2000 + time_array[1] ||= 1 + time_array[2] ||= 1 + return Time.send(ActiveRecord::Base.default_timezone, *time_array) rescue nil + + # Try DateTime instead - the date may be outside the time period support by Time. + DateTime.new(*time_array[0..5]) rescue nil + end + + def cast_to_date(value) + return value if value.is_a?(Date) + return Date.parse(value) rescue nil + end + + def cast_to_datetime(value) + if value.is_a?(Time) + if value.year != 0 and value.month != 0 and value.day != 0 + return value + else + return Time.mktime(2000, 1, 1, value.hour, value.min, value.sec) rescue nil + end + end + if value.is_a?(DateTime) + begin + # Attempt to convert back to a Time, but it could fail for dates significantly in the past/future. + return Time.mktime(value.year, value.mon, value.day, value.hour, value.min, value.sec) + rescue ArgumentError + return value + end + end + + return cast_to_time(value) if value.is_a?(Date) or value.is_a?(String) rescue nil + + return value.is_a?(Date) ? value : nil + end + + # These methods will only allow the adapter to insert binary data with a length of 7K or less + # because of a SQL Server statement length policy. + def self.string_to_binary(value) + '' + end + + end + + def quote(value, column = nil) + return value.quoted_id if value.respond_to?(:quoted_id) + + case value + # SQL Server 2000 doesn't let you insert an integer into a NVARCHAR + # column, so we include Integer here. + when String, ActiveSupport::Multibyte::Chars, Integer + value = value.to_s + if column && column.type == :binary + "'#{quote_string(ArJdbc::MsSQL::Column.string_to_binary(value))}'" # ' (for ruby-mode) + elsif column && [:integer, :float].include?(column.type) + value = column.type == :integer ? value.to_i : value.to_f + value.to_s + elsif !column.respond_to?(:is_utf8?) || column.is_utf8? + "N'#{quote_string(value)}'" # ' (for ruby-mode) + else + super + end + when TrueClass then '1' + when FalseClass then '0' + else super + end + end + + def quote_string(string) + string.gsub(/\'/, "''") + end + + def quote_table_name(name) + quote_column_name(name) + end + + def quote_column_name(name) + "[#{name}]" + end + + def quoted_true + quote true + end + + def quoted_false + quote false + end + + def adapter_name #:nodoc: + 'MsSQL' + end + + def change_order_direction(order) + order.split(",").collect do |fragment| + case fragment + when /\bDESC\b/i then fragment.gsub(/\bDESC\b/i, "ASC") + when /\bASC\b/i then fragment.gsub(/\bASC\b/i, "DESC") + else String.new(fragment).split(',').join(' DESC,') + ' DESC' + end + end.join(",") + end + + def supports_ddl_transactions? + true + end + + def recreate_database(name) + drop_database(name) + create_database(name) + end + + def drop_database(name) + execute "USE master" + execute "DROP DATABASE #{name}" + end + + def create_database(name) + execute "CREATE DATABASE #{name}" + execute "USE #{name}" + end + + def rename_table(name, new_name) + clear_cached_table(name) + execute "EXEC sp_rename '#{name}', '#{new_name}'" + end + + # Adds a new column to the named table. + # See TableDefinition#column for details of the options you can use. + def add_column(table_name, column_name, type, options = {}) + clear_cached_table(table_name) + add_column_sql = "ALTER TABLE #{table_name} ADD #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}" + add_column_options!(add_column_sql, options) + # TODO: Add support to mimic date columns, using constraints to mark them as such in the database + # add_column_sql << " CONSTRAINT ck__#{table_name}__#{column_name}__date_only CHECK ( CONVERT(CHAR(12), #{quote_column_name(column_name)}, 14)='00:00:00:000' )" if type == :date + execute(add_column_sql) + end + + def rename_column(table, column, new_column_name) + clear_cached_table(table) + execute "EXEC sp_rename '#{table}.#{column}', '#{new_column_name}'" + end + + def change_column(table_name, column_name, type, options = {}) #:nodoc: + clear_cached_table(table_name) + change_column_type(table_name, column_name, type, options) + change_column_default(table_name, column_name, options[:default]) if options_include_default?(options) + end + + def change_column_type(table_name, column_name, type, options = {}) #:nodoc: + clear_cached_table(table_name) + sql = "ALTER TABLE #{table_name} ALTER COLUMN #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}" + if options.has_key?(:null) + sql += (options[:null] ? " NULL" : " NOT NULL") + end + execute(sql) + end + + def change_column_default(table_name, column_name, default) #:nodoc: + clear_cached_table(table_name) + remove_default_constraint(table_name, column_name) + unless default.nil? + execute "ALTER TABLE #{table_name} ADD CONSTRAINT DF_#{table_name}_#{column_name} DEFAULT #{quote(default)} FOR #{quote_column_name(column_name)}" + end + end + + def remove_column(table_name, column_name) + clear_cached_table(table_name) + remove_check_constraints(table_name, column_name) + remove_default_constraint(table_name, column_name) + execute "ALTER TABLE #{table_name} DROP COLUMN [#{column_name}]" + end + + def remove_default_constraint(table_name, column_name) + clear_cached_table(table_name) + defaults = select "select def.name from sysobjects def, syscolumns col, sysobjects tab where col.cdefault = def.id and col.name = '#{column_name}' and tab.name = '#{table_name}' and col.id = tab.id" + defaults.each {|constraint| + execute "ALTER TABLE #{table_name} DROP CONSTRAINT #{constraint["name"]}" + } + end + + def remove_check_constraints(table_name, column_name) + clear_cached_table(table_name) + # TODO remove all constraints in single method + constraints = select "SELECT CONSTRAINT_NAME FROM INFORMATION_SCHEMA.CONSTRAINT_COLUMN_USAGE where TABLE_NAME = '#{table_name}' and COLUMN_NAME = '#{column_name}'" + constraints.each do |constraint| + execute "ALTER TABLE #{table_name} DROP CONSTRAINT #{constraint["CONSTRAINT_NAME"]}" + end + end + + def remove_index(table_name, options = {}) + execute "DROP INDEX #{table_name}.#{index_name(table_name, options)}" + end + + def columns(table_name, name = nil) + # It's possible for table_name to be an empty string, or nil, if something attempts to issue SQL + # which doesn't involve a table. IE. "SELECT 1" or "SELECT * from someFunction()". + return [] if table_name.blank? + table_name = table_name.to_s if table_name.is_a?(Symbol) + + # Remove []'s from around the table name, valid in a select statement, but not when matching metadata. + table_name = table_name.gsub(/[\[\]]/, '') + + return [] if table_name =~ /^information_schema\./i + @table_columns = {} unless @table_columns + unless @table_columns[table_name] + @table_columns[table_name] = super + @table_columns[table_name].each do |col| + col.identity = true if col.sql_type =~ /identity/i + col.is_special = true if col.sql_type =~ /text|ntext|image|xml/i + end + end + @table_columns[table_name] + end + + def _execute(sql, name = nil) + # Match the start of the sql to determine appropriate behaviour. Be aware of + # multi-line sql which might begin with 'create stored_proc' and contain 'insert into ...' lines. + # Possible improvements include ignoring comment blocks prior to the first statement. + if sql.lstrip =~ /\Ainsert/i + if query_requires_identity_insert?(sql) + table_name = get_table_name(sql) + with_identity_insert_enabled(table_name) do + id = @connection.execute_insert(sql) + end + else + @connection.execute_insert(sql) + end + elsif sql.lstrip =~ /\A(create|exec)/i + @connection.execute_update(sql) + elsif sql.lstrip =~ /\A\(?\s*(select|show)/i + repair_special_columns(sql) + @connection.execute_query(sql) + else + @connection.execute_update(sql) + end + end + + def select(sql, name = nil) + log(sql, name) do + @connection.execute_query(sql) + end + end + + #SELECT .. FOR UPDATE is not supported on Microsoft SQL Server + def add_lock!(sql, options) + sql + end + + # Turns IDENTITY_INSERT ON for table during execution of the block + # N.B. This sets the state of IDENTITY_INSERT to OFF after the + # block has been executed without regard to its previous state + def with_identity_insert_enabled(table_name, &block) + set_identity_insert(table_name, true) + yield + ensure + set_identity_insert(table_name, false) + end + + def set_identity_insert(table_name, enable = true) + execute "SET IDENTITY_INSERT #{table_name} #{enable ? 'ON' : 'OFF'}" + rescue Exception => e + raise ActiveRecord::ActiveRecordError, "IDENTITY_INSERT could not be turned #{enable ? 'ON' : 'OFF'} for table #{table_name}" + end + + def identity_column(table_name) + columns(table_name).each do |col| + return col.name if col.identity + end + return nil + end + + def query_requires_identity_insert?(sql) + table_name = get_table_name(sql) + id_column = identity_column(table_name) + if sql.strip =~ /insert into [^ ]+ ?\((.+?)\)/i + insert_columns = $1.split(/, */).map(&method(:unquote_column_name)) + return table_name if insert_columns.include?(id_column) + end + end + + def unquote_column_name(name) + if name =~ /^\[.*\]$/ + name[1..-2] + else + name + end + end + + def get_special_columns(table_name) + special = [] + columns(table_name).each do |col| + special << col.name if col.is_special + end + special + end + + def repair_special_columns(sql) + special_cols = get_special_columns(get_table_name(sql)) + for col in special_cols.to_a + sql.gsub!(Regexp.new(" #{col.to_s} = "), " #{col.to_s} LIKE ") + sql.gsub!(/ORDER BY #{col.to_s}/i, '') + end + sql + end + + def determine_order_clause(sql) + return $1 if sql =~ /ORDER BY (.*)$/ + table_name = get_table_name(sql) + "#{table_name}.#{determine_primary_key(table_name)}" + end + + def determine_primary_key(table_name) + primary_key = columns(table_name).detect { |column| column.primary || column.identity } + return primary_key.name if primary_key + # Look for an id column. Return it, without changing case, to cover dbs with a case-sensitive collation. + columns(table_name).each { |column| return column.name if column.name =~ /^id$/i } + # Give up and provide something which is going to crash almost certainly + columns(table_name)[0].name + end + + def clear_cached_table(name) + (@table_columns ||= {}).delete(name.to_s) + end + end +end + diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/mssql/connection_methods.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/mssql/connection_methods.rb new file mode 100644 index 00000000000..46d3eba8162 --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/mssql/connection_methods.rb @@ -0,0 +1,30 @@ +class ActiveRecord::Base + class << self + def mssql_connection(config) + require "arjdbc/mssql" + config[:host] ||= "localhost" + config[:port] ||= 1433 + config[:driver] ||= "net.sourceforge.jtds.jdbc.Driver" + + url = "jdbc:jtds:sqlserver://#{config[:host]}:#{config[:port]}/#{config[:database]}" + + # Instance is often a preferrable alternative to port when dynamic ports are used. + # If instance is specified then port is essentially ignored. + url << ";instance=#{config[:instance]}" if config[:instance] + + # This will enable windows domain-based authentication and will require the JTDS native libraries be available. + url << ";domain=#{config[:domain]}" if config[:domain] + + # AppName is shown in sql server as additional information against the connection. + url << ";appname=#{config[:appname]}" if config[:appname] + config[:url] ||= url + + if !config[:domain] + config[:username] ||= "sa" + config[:password] ||= "" + end + jdbc_connection(config) + end + alias_method :jdbcmssql_connection, :mssql_connection + end +end diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/mssql/limit_helpers.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/mssql/limit_helpers.rb new file mode 100644 index 00000000000..85e57ac5935 --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/mssql/limit_helpers.rb @@ -0,0 +1,98 @@ +module ::ArJdbc + module MsSQL + module LimitHelpers + module_function + def get_table_name(sql) + if sql =~ /^\s*insert\s+into\s+([^\(\s,]+)\s*|^\s*update\s+([^\(\s,]+)\s*/i + $1 + elsif sql =~ /\bfrom\s+([^\(\s,]+)\s*/i + $1 + else + nil + end + end + + module SqlServer2000ReplaceLimitOffset + module_function + def replace_limit_offset!(sql, limit, offset, order) + if limit + offset ||= 0 + start_row = offset + 1 + end_row = offset + limit.to_i + find_select = /\b(SELECT(?:\s+DISTINCT)?)\b(.*)/im + whole, select, rest_of_query = find_select.match(sql).to_a + if (start_row == 1) && (end_row ==1) + new_sql = "#{select} TOP 1 #{rest_of_query}" + sql.replace(new_sql) + else + #UGLY + #KLUDGY? + #removing out stuff before the FROM... + rest = rest_of_query[/FROM/i=~ rest_of_query.. -1] + #need the table name for avoiding amiguity + table_name = LimitHelpers.get_table_name(sql) + primary_key = order[/(\w*id\w*)/i] + #I am not sure this will cover all bases. but all the tests pass + new_order = "ORDER BY #{order}, #{table_name}.#{primary_key}" if order.index("#{table_name}.#{primary_key}").nil? + new_order ||= order + + if (rest_of_query.match(/WHERE/).nil?) + new_sql = "#{select} TOP #{limit} #{rest_of_query} WHERE #{table_name}.#{primary_key} NOT IN (#{select} TOP #{offset} #{table_name}.#{primary_key} #{rest} #{new_order}) #{order} " + else + new_sql = "#{select} TOP #{limit} #{rest_of_query} AND #{table_name}.#{primary_key} NOT IN (#{select} TOP #{offset} #{table_name}.#{primary_key} #{rest} #{new_order}) #{order} " + end + + sql.replace(new_sql) + end + end + sql + end + end + + module SqlServer2000AddLimitOffset + def add_limit_offset!(sql, options) + if options[:limit] + order = "ORDER BY #{options[:order] || determine_order_clause(sql)}" + sql.sub!(/ ORDER BY.*$/i, '') + SqlServerReplaceLimitOffset.replace_limit_offset!(sql, options[:limit], options[:offset], order) + end + end + end + + module SqlServerReplaceLimitOffset + module_function + def replace_limit_offset!(sql, limit, offset, order) + if limit + offset ||= 0 + start_row = offset + 1 + end_row = offset + limit.to_i + find_select = /\b(SELECT(?:\s+DISTINCT)?)\b(.*)/im + whole, select, rest_of_query = find_select.match(sql).to_a + rest_of_query.strip! + if rest_of_query[0] == "1" + rest_of_query[0] = "*" + end + if rest_of_query[0] == "*" + from_table = LimitHelpers.get_table_name(rest_of_query) + rest_of_query = from_table + '.' + rest_of_query + end + new_sql = "#{select} t.* FROM (SELECT ROW_NUMBER() OVER(#{order}) AS _row_num, #{rest_of_query}" + new_sql << ") AS t WHERE t._row_num BETWEEN #{start_row.to_s} AND #{end_row.to_s}" + sql.replace(new_sql) + end + sql + end + end + + module SqlServerAddLimitOffset + def add_limit_offset!(sql, options) + if options[:limit] + order = "ORDER BY #{options[:order] || determine_order_clause(sql)}" + sql.sub!(/ ORDER BY.*$/i, '') + SqlServerReplaceLimitOffset.replace_limit_offset!(sql, options[:limit], options[:offset], order) + end + end + end + end + end +end diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-0.9.0.1/lib/jdbc_adapter/tsql_helper.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/mssql/tsql_helper.rb index 37de11d1080..ea20e8fcf4b 100755..100644 --- a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-0.9.0.1/lib/jdbc_adapter/tsql_helper.rb +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/mssql/tsql_helper.rb @@ -10,8 +10,10 @@ module TSqlMethods end def type_to_sql(type, limit = nil, precision = nil, scale = nil) #:nodoc: + limit = nil if %w(text binary).include? type.to_s + return 'uniqueidentifier' if (type.to_s == 'uniqueidentifier') return super unless type.to_s == 'integer' - + if limit.nil? || limit == 4 'int' elsif limit == 2 diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/mysql.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/mysql.rb new file mode 100644 index 00000000000..bc9aaa11828 --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/mysql.rb @@ -0,0 +1,4 @@ +require 'arjdbc/jdbc' +jdbc_require_driver 'jdbc/mysql' +require 'arjdbc/mysql/connection_methods' +require 'arjdbc/mysql/adapter' diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/mysql/adapter.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/mysql/adapter.rb new file mode 100644 index 00000000000..891d59ae89d --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/mysql/adapter.rb @@ -0,0 +1,440 @@ +require 'active_record/connection_adapters/abstract/schema_definitions' + +module ::ArJdbc + module MySQL + def self.column_selector + [/mysql/i, lambda {|cfg,col| col.extend(::ArJdbc::MySQL::Column)}] + end + + def self.extended(adapter) + adapter.configure_connection + end + + def configure_connection + execute("SET SQL_AUTO_IS_NULL=0") + end + + def self.jdbc_connection_class + ::ActiveRecord::ConnectionAdapters::MySQLJdbcConnection + end + + module Column + def extract_default(default) + if sql_type =~ /blob/i || type == :text + if default.blank? + return null ? nil : '' + else + raise ArgumentError, "#{type} columns cannot have a default value: #{default.inspect}" + end + elsif missing_default_forged_as_empty_string?(default) + nil + else + super + end + end + + def has_default? + return false if sql_type =~ /blob/i || type == :text #mysql forbids defaults on blob and text columns + super + end + + def simplified_type(field_type) + case field_type + when /tinyint\(1\)|bit/i then :boolean + when /enum/i then :string + when /decimal/i then :decimal + else + super + end + end + + def extract_limit(sql_type) + case sql_type + when /blob|text/i + case sql_type + when /tiny/i + 255 + when /medium/i + 16777215 + when /long/i + 2147483647 # mysql only allows 2^31-1, not 2^32-1, somewhat inconsistently with the tiny/medium/normal cases + else + nil # we could return 65535 here, but we leave it undecorated by default + end + when /^bigint/i; 8 + when /^int/i; 4 + when /^mediumint/i; 3 + when /^smallint/i; 2 + when /^tinyint/i; 1 + when /^(bool|date|float|int|time)/i + nil + else + super + end + end + + # MySQL misreports NOT NULL column default when none is given. + # We can't detect this for columns which may have a legitimate '' + # default (string) but we can for others (integer, datetime, boolean, + # and the rest). + # + # Test whether the column has default '', is not null, and is not + # a type allowing default ''. + def missing_default_forged_as_empty_string?(default) + type != :string && !null && default == '' + end + end + + + def modify_types(tp) + tp[:primary_key] = "int(11) DEFAULT NULL auto_increment PRIMARY KEY" + tp[:decimal] = { :name => "decimal" } + tp[:timestamp] = { :name => "datetime" } + tp[:datetime][:limit] = nil + + # sonar + # Ticket http://tools.assembla.com/sonar/ticket/200 + # Problem with mysql TEXT columns. ActiveRecord :text type is mapped to TEXT type (65535 characters). + # But we would like the bigger MEDIUMTEXT for the snapshot_sources table (16777215 characters). + # This hack works only for ActiveRecord-JDBC (Jruby use). + # See http://www.headius.com/jrubywiki/index.php/Adding_Datatypes_to_ActiveRecord-JDBC + tp[:text] = { :name => "mediumtext" } + tp[:binary] = { :name => "longblob" } + tp[:big_integer] = { :name => "bigint"} + # /sonar + + tp + end + + def adapter_name #:nodoc: + 'MySQL' + end + + def arel2_visitors + {'jdbcmysql' => ::Arel::Visitors::MySQL} + end + + def case_sensitive_equality_operator + "= BINARY" + end + + def limited_update_conditions(where_sql, quoted_table_name, quoted_primary_key) + where_sql + end + + # QUOTING ================================================== + + def quote(value, column = nil) + return value.quoted_id if value.respond_to?(:quoted_id) + + if column && column.type == :primary_key + value.to_s + elsif column && String === value && column.type == :binary && column.class.respond_to?(:string_to_binary) + s = column.class.string_to_binary(value).unpack("H*")[0] + "x'#{s}'" + elsif BigDecimal === value + "'#{value.to_s("F")}'" + else + super + end + end + + def quoted_true + "1" + end + + def quoted_false + "0" + end + + def begin_db_transaction #:nodoc: + @connection.begin + rescue Exception + # Transactions aren't supported + end + + def commit_db_transaction #:nodoc: + @connection.commit + rescue Exception + # Transactions aren't supported + end + + def rollback_db_transaction #:nodoc: + @connection.rollback + rescue Exception + # Transactions aren't supported + end + + def supports_savepoints? #:nodoc: + true + end + + def create_savepoint + execute("SAVEPOINT #{current_savepoint_name}") + end + + def rollback_to_savepoint + execute("ROLLBACK TO SAVEPOINT #{current_savepoint_name}") + end + + def release_savepoint + execute("RELEASE SAVEPOINT #{current_savepoint_name}") + end + + def disable_referential_integrity(&block) #:nodoc: + old = select_value("SELECT @@FOREIGN_KEY_CHECKS") + begin + update("SET FOREIGN_KEY_CHECKS = 0") + yield + ensure + update("SET FOREIGN_KEY_CHECKS = #{old}") + end + end + + # SCHEMA STATEMENTS ======================================== + + def structure_dump #:nodoc: + if supports_views? + sql = "SHOW FULL TABLES WHERE Table_type = 'BASE TABLE'" + else + sql = "SHOW TABLES" + end + + select_all(sql).inject("") do |structure, table| + table.delete('Table_type') + + hash = show_create_table(table.to_a.first.last) + + if(table = hash["Create Table"]) + structure += table + ";\n\n" + elsif(view = hash["Create View"]) + structure += view + ";\n\n" + end + end + end + + def jdbc_columns(table_name, name = nil)#:nodoc: + sql = "SHOW FIELDS FROM #{quote_table_name(table_name)}" + execute(sql, :skip_logging).map do |field| + ::ActiveRecord::ConnectionAdapters::MysqlColumn.new(field["Field"], field["Default"], field["Type"], field["Null"] == "YES") + end + end + + def recreate_database(name, options = {}) #:nodoc: + drop_database(name) + create_database(name, options) + end + + def create_database(name, options = {}) #:nodoc: + if options[:collation] + execute "CREATE DATABASE `#{name}` DEFAULT CHARACTER SET `#{options[:charset] || 'utf8'}` COLLATE `#{options[:collation]}`" + else + execute "CREATE DATABASE `#{name}` DEFAULT CHARACTER SET `#{options[:charset] || 'utf8'}`" + end + end + + def drop_database(name) #:nodoc: + execute "DROP DATABASE IF EXISTS `#{name}`" + end + + def current_database + select_one("SELECT DATABASE() as db")["db"] + end + + def create_table(name, options = {}) #:nodoc: + #sonar - force UTF8 + #super(name, {:options => "ENGINE=InnoDB"}.merge(options)) + super(name, {:options => "ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_bin"}.merge(options)) + #/sonar + end + + def rename_table(name, new_name) + execute "RENAME TABLE #{quote_table_name(name)} TO #{quote_table_name(new_name)}" + end + + def add_column(table_name, column_name, type, options = {}) + add_column_sql = "ALTER TABLE #{quote_table_name(table_name)} ADD #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}" + add_column_options!(add_column_sql, options) + add_column_position!(add_column_sql, options) + execute(add_column_sql) + end + + def change_column_default(table_name, column_name, default) #:nodoc: + column = column_for(table_name, column_name) + change_column table_name, column_name, column.sql_type, :default => default + end + + def change_column_null(table_name, column_name, null, default = nil) + column = column_for(table_name, column_name) + + unless null || default.nil? + execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL") + end + + change_column table_name, column_name, column.sql_type, :null => null + end + + def change_column(table_name, column_name, type, options = {}) #:nodoc: + column = column_for(table_name, column_name) + + unless options_include_default?(options) + options[:default] = column.default + end + + unless options.has_key?(:null) + options[:null] = column.null + end + + change_column_sql = "ALTER TABLE #{quote_table_name(table_name)} CHANGE #{quote_column_name(column_name)} #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}" + add_column_options!(change_column_sql, options) + add_column_position!(change_column_sql, options) + execute(change_column_sql) + end + + def rename_column(table_name, column_name, new_column_name) #:nodoc: + options = {} + if column = columns(table_name).find { |c| c.name == column_name.to_s } + options[:default] = column.default + options[:null] = column.null + else + raise ActiveRecord::ActiveRecordError, "No such column: #{table_name}.#{column_name}" + end + current_type = select_one("SHOW COLUMNS FROM #{quote_table_name(table_name)} LIKE '#{column_name}'")["Type"] + rename_column_sql = "ALTER TABLE #{quote_table_name(table_name)} CHANGE #{quote_column_name(column_name)} #{quote_column_name(new_column_name)} #{current_type}" + add_column_options!(rename_column_sql, options) + execute(rename_column_sql) + end + + def add_limit_offset!(sql, options) #:nodoc: + limit, offset = options[:limit], options[:offset] + if limit && offset + sql << " LIMIT #{offset.to_i}, #{sanitize_limit(limit)}" + elsif limit + sql << " LIMIT #{sanitize_limit(limit)}" + elsif offset + sql << " OFFSET #{offset.to_i}" + end + sql + end + + def show_variable(var) + res = execute("show variables like '#{var}'") + row = res.detect {|row| row["Variable_name"] == var } + row && row["Value"] + end + + def charset + show_variable("character_set_database") + end + + def collation + show_variable("collation_database") + end + + def type_to_sql(type, limit = nil, precision = nil, scale = nil) + return super unless type.to_s == 'integer' + + case limit + when 1; 'tinyint' + when 2; 'smallint' + when 3; 'mediumint' + when nil, 4, 11; 'int(11)' # compatibility with MySQL default + when 5..8; 'bigint' + else raise(ActiveRecordError, "No integer type has byte size #{limit}") + end + end + + def add_column_position!(sql, options) + if options[:first] + sql << " FIRST" + elsif options[:after] + sql << " AFTER #{quote_column_name(options[:after])}" + end + end + + protected + def translate_exception(exception, message) + return super unless exception.respond_to?(:errno) + + case exception.errno + when 1062 + ::ActiveRecord::RecordNotUnique.new(message, exception) + when 1452 + ::ActiveRecord::InvalidForeignKey.new(message, exception) + else + super + end + end + + private + def column_for(table_name, column_name) + unless column = columns(table_name).find { |c| c.name == column_name.to_s } + raise "No such column: #{table_name}.#{column_name}" + end + column + end + + def show_create_table(table) + select_one("SHOW CREATE TABLE #{quote_table_name(table)}") + end + + def supports_views? + false + end + end +end + +module ActiveRecord::ConnectionAdapters + # Remove any vestiges of core/Ruby MySQL adapter + remove_const(:MysqlColumn) if const_defined?(:MysqlColumn) + remove_const(:MysqlAdapter) if const_defined?(:MysqlAdapter) + + class MysqlColumn < JdbcColumn + include ArJdbc::MySQL::Column + + def initialize(name, *args) + if Hash === name + super + else + super(nil, name, *args) + end + end + + def call_discovered_column_callbacks(*) + end + end + + class MysqlAdapter < JdbcAdapter + include ArJdbc::MySQL + + def initialize(*args) + super + configure_connection + end + + def adapter_spec(config) + # return nil to avoid extending ArJdbc::MySQL, which we've already done + end + + def jdbc_connection_class(spec) + ::ArJdbc::MySQL.jdbc_connection_class + end + + def jdbc_column_class + ActiveRecord::ConnectionAdapters::MysqlColumn + end + + alias_chained_method :columns, :query_cache, :jdbc_columns + end +end + +module Mysql # :nodoc: + remove_const(:Error) if const_defined?(:Error) + + class Error < ::ActiveRecord::JDBCError + end + + def self.client_version + 50400 # faked out for AR tests + end +end diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/mysql/connection_methods.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/mysql/connection_methods.rb new file mode 100644 index 00000000000..a1267f6da01 --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/mysql/connection_methods.rb @@ -0,0 +1,27 @@ +# Don't need to load native mysql adapter +$LOADED_FEATURES << "active_record/connection_adapters/mysql_adapter.rb" +$LOADED_FEATURES << "active_record/connection_adapters/mysql2_adapter.rb" + +class ActiveRecord::Base + class << self + def mysql_connection(config) + require "arjdbc/mysql" + config[:port] ||= 3306 + options = (config[:options] ||= {}) + options['zeroDateTimeBehavior'] ||= 'convertToNull' + options['jdbcCompliantTruncation'] ||= 'false' + options['useUnicode'] ||= 'true' + options['characterEncoding'] = config[:encoding] || 'utf8' + config[:url] ||= "jdbc:mysql://#{config[:host]}:#{config[:port]}/#{config[:database]}" + config[:driver] ||= "com.mysql.jdbc.Driver" + config[:adapter_class] = ActiveRecord::ConnectionAdapters::MysqlAdapter + connection = jdbc_connection(config) + ::ArJdbc::MySQL.kill_cancel_timer(connection.raw_connection) + connection + end + alias_method :jdbcmysql_connection, :mysql_connection + alias_method :mysql2_connection, :mysql_connection + end +end + + diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/oracle.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/oracle.rb new file mode 100644 index 00000000000..5c0645defab --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/oracle.rb @@ -0,0 +1,3 @@ +require 'arjdbc/jdbc' +require 'arjdbc/oracle/connection_methods' +require 'arjdbc/oracle/adapter' diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-0.9.0.1/lib/jdbc_adapter/jdbc_oracle.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/oracle/adapter.rb index 57cf8778622..d0f08a2974b 100755..100644 --- a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-0.9.0.1/lib/jdbc_adapter/jdbc_oracle.rb +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/oracle/adapter.rb @@ -1,157 +1,183 @@ -module ::ActiveRecord - class Base - def after_save_with_oracle_lob() #:nodoc: - if connection.is_a?(JdbcSpec::Oracle) - self.class.columns.select { |c| c.sql_type =~ /LOB\(|LOB$/i }.each { |c| - value = self[c.name] - value = value.to_yaml if unserializable_attribute?(c.name, c) - next if value.nil? || (value == '') - - connection.write_large_object(c.type == :binary, c.name, self.class.table_name, self.class.primary_key, quote_value(id), value) - } - end - end - end +module ActiveRecord::ConnectionAdapters + OracleAdapter = Class.new(AbstractAdapter) unless const_defined?(:OracleAdapter) end -module ::JdbcSpec - module ActiveRecordExtensions - def oracle_connection(config) - config[:port] ||= 1521 - config[:url] ||= "jdbc:oracle:thin:@#{config[:host]}:#{config[:port]}:#{config[:database]}" - config[:driver] ||= "oracle.jdbc.driver.OracleDriver" - jdbc_connection(config) - end - end - +module ::ArJdbc module Oracle def self.extended(mod) - ActiveRecord::Base.after_save :after_save_with_oracle_lob unless @lob_callback_added - @lob_callback_added = true + unless @lob_callback_added + ActiveRecord::Base.class_eval do + def after_save_with_oracle_lob + self.class.columns.select { |c| c.sql_type =~ /LOB\(|LOB$/i }.each do |c| + value = self[c.name] + value = value.to_yaml if unserializable_attribute?(c.name, c) + next if value.nil? || (value == '') + + connection.write_large_object(c.type == :binary, c.name, self.class.table_name, self.class.primary_key, quote_value(id), value) + end + end + end + + ActiveRecord::Base.after_save :after_save_with_oracle_lob + @lob_callback_added = true + end + require 'arjdbc/jdbc/quoted_primary_key' + ActiveRecord::Base.extend ArJdbc::QuotedPrimaryKeyExtension + (class << mod; self; end).class_eval do + alias_chained_method :insert, :query_dirty, :ora_insert + alias_chained_method :columns, :query_cache, :ora_columns + end end def self.column_selector - [/oracle/i, lambda {|cfg,col| col.extend(::JdbcSpec::Oracle::Column)}] - end - - def self.adapter_selector - [/oracle/i, lambda {|cfg,adapt| adapt.extend(::JdbcSpec::Oracle) -=begin - (adapt.methods - %w(send __send__ id class methods is_a? kind_of? verify! active?)).each do |name| - new_name = "__#{name}" - (class << adapt; self; end).send :alias_method, new_name, name - (class << adapt; self; end).send :define_method, name do |*args| - puts "#{name}(#{args.inspect})" - adapt.send new_name, *args - end - end -=end - }] - end - + [/oracle/i, lambda {|cfg,col| col.extend(::ArJdbc::Oracle::Column)}] + end + + def self.jdbc_connection_class + ::ActiveRecord::ConnectionAdapters::OracleJdbcConnection + end + module Column + def primary=(val) + super + if val && @sql_type =~ /^NUMBER$/i + @type = :integer + end + end + def type_cast(value) return nil if value.nil? case type - when :string then value - when :integer then defined?(value.to_i) ? value.to_i : (value ? 1 : 0) - when :primary_key then defined?(value.to_i) ? value.to_i : (value ? 1 : 0) - when :float then value.to_f - when :datetime then JdbcSpec::Oracle::Column.cast_to_date_or_time(value) - when :time then JdbcSpec::Oracle::Column.cast_to_time(value) - when :decimal then self.class.value_to_decimal(value) - when :boolean then self.class.value_to_boolean(value) - else value + when :datetime then ArJdbc::Oracle::Column.string_to_time(value, self.class) + else + super end end - + def type_cast_code(var_name) case type - when :string then nil - when :integer then "(#{var_name}.to_i rescue #{var_name} ? 1 : 0)" - when :primary_key then "(#{var_name}.to_i rescue #{var_name} ? 1 : 0)" - when :float then "#{var_name}.to_f" - when :datetime then "JdbcSpec::Oracle::Column.cast_to_date_or_time(#{var_name})" - when :time then "JdbcSpec::Oracle::Column.cast_to_time(#{var_name})" - when :decimal then "#{self.class.name}.value_to_decimal(#{var_name})" - when :boolean then "#{self.class.name}.value_to_boolean(#{var_name})" - else nil + when :datetime then "ArJdbc::Oracle::Column.string_to_time(#{var_name}, self.class)" + else + super end - end + end + + def self.string_to_time(string, klass) + time = klass.string_to_time(string) + guess_date_or_time(time) + end + + def self.guess_date_or_time(value) + return value if Date === value + (value && value.hour == 0 && value.min == 0 && value.sec == 0) ? + Date.new(value.year, value.month, value.day) : value + end private def simplified_type(field_type) case field_type - when /^number\(1\)$/i : :boolean - when /char/i : :string - when /float|double/i : :float - when /int/i : :integer - when /num|dec|real/i : @scale == 0 ? :integer : :decimal - when /date|time/i : :datetime - when /clob/i : :text - when /blob/i : :binary + when /^number\(1\)$/i then :boolean + when /char/i then :string + when /float|double/i then :float + when /int/i then :integer + when /num|dec|real/i then extract_scale(field_type) == 0 ? :integer : :decimal + when /date|time/i then :datetime + when /clob/i then :text + when /blob/i then :binary end end - def self.cast_to_date_or_time(value) - return value if value.is_a? Date - return nil if value.blank? - guess_date_or_time((value.is_a? Time) ? value : cast_to_time(value)) - end + # Post process default value from JDBC into a Rails-friendly format (columns{-internal}) + def default_value(value) + return nil unless value - def self.cast_to_time(value) - return value if value.is_a? Time - time_array = ParseDate.parsedate value - time_array[0] ||= 2000; time_array[1] ||= 1; time_array[2] ||= 1; - Time.send(ActiveRecord::Base.default_timezone, *time_array) rescue nil - end + # Not sure why we need this for Oracle? + value = value.strip - def self.guess_date_or_time(value) - (value.hour == 0 and value.min == 0 and value.sec == 0) ? - Date.new(value.year, value.month, value.day) : value + return nil if value == "null" + + # sysdate default should be treated like a null value + return nil if value.downcase == "sysdate" + + # jdbc returns column default strings with actual single quotes around the value. + return $1 if value =~ /^'(.*)'$/ + + value end end + def adapter_name + 'Oracle' + end + + def arel2_visitors + { 'oracle' => Arel::Visitors::Oracle } + end + + # TODO: use this instead of the QuotedPrimaryKey logic and execute_id_insert? + # def prefetch_primary_key?(table_name = nil) + # columns(table_name).detect {|c| c.primary } if table_name + # end + def table_alias_length 30 end - def default_sequence_name(table, column) #:nodoc: + def default_sequence_name(table, column = nil) #:nodoc: "#{table}_seq" end - + def create_table(name, options = {}) #:nodoc: super(name, options) - seq_name = options[:sequence_name] || "#{name}_seq" + seq_name = options[:sequence_name] || default_sequence_name(name) + start_value = options[:sequence_start_value] || 10000 raise ActiveRecord::StatementInvalid.new("name #{seq_name} too long") if seq_name.length > table_alias_length - execute "CREATE SEQUENCE #{seq_name} START WITH 10000" unless options[:id] == false + execute "CREATE SEQUENCE #{seq_name} START WITH #{start_value}" unless options[:id] == false end def rename_table(name, new_name) #:nodoc: execute "RENAME #{name} TO #{new_name}" execute "RENAME #{name}_seq TO #{new_name}_seq" rescue nil - end + end def drop_table(name, options = {}) #:nodoc: - super(name) - seq_name = options[:sequence_name] || "#{name}_seq" + super(name) rescue nil + seq_name = options[:sequence_name] || default_sequence_name(name) execute "DROP SEQUENCE #{seq_name}" rescue nil end def recreate_database(name) tables.each{ |table| drop_table(table) } end - - def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) #:nodoc: - if pk.nil? # Who called us? What does the sql look like? No idea! - execute sql, name - elsif id_value # Pre-assigned id + + def drop_database(name) + recreate_database(name) + end + + def next_sequence_value(sequence_name) + # avoid #select or #select_one so that the sequence values aren't cached + execute("select #{sequence_name}.nextval id from dual").first['id'].to_i + end + + def sql_literal?(value) + defined?(::Arel::SqlLiteral) && ::Arel::SqlLiteral === value + end + + def ora_insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) #:nodoc: + if (id_value && !sql_literal?(id_value)) || pk.nil? + # Pre-assigned id or table without a primary key + # Presence of #to_sql means an Arel literal bind variable + # that should use #execute_id_insert below execute sql, name - else # Assume the sql contains a bind-variable for the id - id_value = select_one("select #{sequence_name}.nextval id from dual")['id'].to_i - log(sql, name) { + else + # Assume the sql contains a bind-variable for the id + # Extract the table from the insert sql. Yuck. + table = sql.split(" ", 4)[2].gsub('"', '') + sequence_name ||= default_sequence_name(table) + id_value = next_sequence_value(sequence_name) + log(sql, name) do @connection.execute_id_insert(sql,id_value) - } + end end id_value end @@ -159,21 +185,19 @@ module ::JdbcSpec def indexes(table, name = nil) @connection.indexes(table, name, @connection.connection.meta_data.user_name) end - + def _execute(sql, name = nil) case sql.strip - when /\A\(?\s*(select|show)/i: + when /\A\(?\s*(select|show)/i then @connection.execute_query(sql) else @connection.execute_update(sql) end end - + def modify_types(tp) tp[:primary_key] = "NUMBER(38) NOT NULL PRIMARY KEY" tp[:integer] = { :name => "NUMBER", :limit => 38 } - tp[:datetime] = { :name => "DATE" } - tp[:timestamp] = { :name => "DATE" } tp[:time] = { :name => "DATE" } tp[:date] = { :name => "DATE" } tp @@ -181,7 +205,7 @@ module ::JdbcSpec def add_limit_offset!(sql, options) #:nodoc: offset = options[:offset] || 0 - + if limit = options[:limit] sql.replace "select * from (select raw_sql_.*, rownum raw_rnum_ from (#{sql}) raw_sql_ where rownum <= #{offset+limit}) where raw_rnum_ > #{offset}" elsif offset > 0 @@ -204,7 +228,7 @@ module ::JdbcSpec def add_column_options!(sql, options) #:nodoc: # handle case of defaults for CLOB columns, which would otherwise get "quoted" incorrectly if options_include_default?(options) && (column = options[:column]) && column.type == :text - sql << " DEFAULT #{quote(options.delete(:default))}" + sql << " DEFAULT #{quote(options.delete(:default))}" end super end @@ -214,7 +238,7 @@ module ::JdbcSpec add_column_options!(change_column_sql, options) execute(change_column_sql) end - + def rename_column(table_name, column_name, new_column_name) #:nodoc: execute "ALTER TABLE #{table_name} RENAME COLUMN #{column_name} to #{new_column_name}" end @@ -229,24 +253,24 @@ module ::JdbcSpec end select_all("select table_name from user_tables").inject(s) do |structure, table| - ddl = "create table #{table.to_a.first.last} (\n " + ddl = "create table #{table.to_a.first.last} (\n " cols = select_all(%Q{ select column_name, data_type, data_length, data_precision, data_scale, data_default, nullable from user_tab_columns where table_name = '#{table.to_a.first.last}' order by column_id }).map do |row| - row = row.inject({}) do |h,args| + row = row.inject({}) do |h,args| h[args[0].downcase] = args[1] - h + h end - col = "#{row['column_name'].downcase} #{row['data_type'].downcase}" + col = "#{row['column_name'].downcase} #{row['data_type'].downcase}" if row['data_type'] =='NUMBER' and !row['data_precision'].nil? col << "(#{row['data_precision'].to_i}" col << ",#{row['data_scale'].to_i}" if !row['data_scale'].nil? col << ')' elsif row['data_type'].include?('CHAR') - col << "(#{row['data_length'].to_i})" + col << "(#{row['data_length'].to_i})" end col << " default #{row['data_default']}" if !row['data_default'].nil? col << ' not null' if row['nullable'] == 'N' @@ -267,13 +291,13 @@ module ::JdbcSpec drop << "drop table #{table.to_a.first.last} cascade constraints;\n\n" end end - + # SELECT DISTINCT clause for a given set of columns and a given ORDER BY clause. # # Oracle requires the ORDER BY columns to be in the SELECT list for DISTINCT # queries. However, with those columns included in the SELECT DISTINCT list, you # won't actually get a distinct list of the column you want (presuming the column - # has duplicates with multiple values for the ordered-by columns. So we use the + # has duplicates with multiple values for the ordered-by columns. So we use the # FIRST_VALUE function to get a single (first) value for each column, effectively # making every row the same. # @@ -292,7 +316,7 @@ module ::JdbcSpec end # ORDER BY clause for the passed order option. - # + # # Uses column aliases as defined by #distinct. def add_order_by_for_association_limiting!(sql, options) return sql if options[:order].blank? @@ -303,25 +327,43 @@ module ::JdbcSpec sql << "ORDER BY #{order}" end - - + + def tables + @connection.tables(nil, oracle_schema) + end + + def ora_columns(table_name, name=nil) + @connection.columns_internal(table_name, name, oracle_schema) + end + # QUOTING ================================================== # # see: abstract/quoting.rb - - # camelCase column names need to be quoted; not that anyone using Oracle - # would really do this, but handling this case means we pass the test... + + # See ACTIVERECORD_JDBC-33 for details -- better to not quote + # table names, esp. if they have schemas. + def quote_table_name(name) #:nodoc: + name.to_s + end + + # Camelcase column names need to be quoted. + # Nonquoted identifiers can contain only alphanumeric characters from your + # database character set and the underscore (_), dollar sign ($), and pound sign (#). + # Database links can also contain periods (.) and "at" signs (@). + # Oracle strongly discourages you from using $ and # in nonquoted identifiers. + # Source: http://download.oracle.com/docs/cd/B28359_01/server.111/b28286/sql_elements008.htm def quote_column_name(name) #:nodoc: - name.to_s =~ /[A-Z]/ ? "\"#{name}\"" : name.to_s + name.to_s =~ /^[a-z0-9_$#]+$/ ? name.to_s : "\"#{name}\"" end def quote_string(string) #:nodoc: string.gsub(/'/, "''") end - + def quote(value, column = nil) #:nodoc: - return value.quoted_id if value.respond_to?(:quoted_id) - + # Arel 2 passes SqlLiterals through + return value if sql_literal?(value) + if column && [:text, :binary].include?(column.type) if /(.*?)\([0-9]+\)/ =~ column.sql_type %Q{empty_#{ $1.downcase }()} @@ -329,35 +371,39 @@ module ::JdbcSpec %Q{empty_#{ column.sql_type.downcase rescue 'blob' }()} end else - if column && column.type == :primary_key - return value.to_s + if column.respond_to?(:primary) && column.primary && column.klass != String + return value.to_i.to_s end - case value - when String, ActiveSupport::Multibyte::Chars - if column.type == :datetime - %Q{TIMESTAMP'#{value}'} - else - %Q{'#{quote_string(value)}'} - end - when NilClass : 'null' - when TrueClass : '1' - when FalseClass : '0' - when Numeric : value.to_s - when Date, Time : %Q{TIMESTAMP'#{value.strftime("%Y-%m-%d %H:%M:%S")}'} - else %Q{'#{quote_string(value.to_yaml)}'} + quoted = super + if value.acts_like?(:date) + quoted = %Q{DATE'#{quoted_date(value)}'} + elsif value.acts_like?(:time) + quoted = %Q{TIMESTAMP'#{quoted_date(value)}'} end + quoted end end - + def quoted_true #:nodoc: '1' end - + def quoted_false #:nodoc: '0' end - + private + # In Oracle, schemas are usually created under your username: + # http://www.oracle.com/technology/obe/2day_dba/schema/schema.htm + # But allow separate configuration as "schema:" anyway (GH #53) + def oracle_schema + if @config[:schema] + @config[:schema].to_s + elsif @config[:username] + @config[:username].to_s + end + end + def select(sql, name=nil) records = execute(sql,name) records.each do |col| @@ -367,3 +413,4 @@ module ::JdbcSpec end end end + diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/oracle/connection_methods.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/oracle/connection_methods.rb new file mode 100644 index 00000000000..2f35ba4197e --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/oracle/connection_methods.rb @@ -0,0 +1,11 @@ +class ActiveRecord::Base + class << self + def oracle_connection(config) + config[:port] ||= 1521 + config[:url] ||= "jdbc:oracle:thin:@#{config[:host]}:#{config[:port]}:#{config[:database]}" + config[:driver] ||= "oracle.jdbc.driver.OracleDriver" + jdbc_connection(config) + end + end +end + diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/postgresql.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/postgresql.rb new file mode 100644 index 00000000000..a707f0c00e6 --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/postgresql.rb @@ -0,0 +1,4 @@ +require 'arjdbc/jdbc' +jdbc_require_driver 'jdbc/postgres' +require 'arjdbc/postgresql/connection_methods' +require 'arjdbc/postgresql/adapter' diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/postgresql/adapter.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/postgresql/adapter.rb new file mode 100644 index 00000000000..28a42647bc5 --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/postgresql/adapter.rb @@ -0,0 +1,608 @@ +module ActiveRecord::ConnectionAdapters + PostgreSQLAdapter = Class.new(AbstractAdapter) unless const_defined?(:PostgreSQLAdapter) +end + +module ::ArJdbc + module PostgreSQL + def self.extended(mod) + (class << mod; self; end).class_eval do + alias_chained_method :insert, :query_dirty, :pg_insert + alias_chained_method :columns, :query_cache, :pg_columns + end + end + + def self.column_selector + [/postgre/i, lambda {|cfg,col| col.extend(::ArJdbc::PostgreSQL::Column)}] + end + + def self.jdbc_connection_class + ::ActiveRecord::ConnectionAdapters::PostgresJdbcConnection + end + + module Column + def type_cast(value) + case type + when :boolean then cast_to_boolean(value) + else super + end + end + + def extract_limit(sql_type) + case sql_type + when /^int2/i; 2 + when /^smallint/i; 2 + when /^int4/i; nil + when /^integer/i; nil + when /^int8/i; 8 + when /^bigint/i; 8 + when /^(bool|text|date|time|bytea)/i; nil # ACTIVERECORD_JDBC-135,139 + else super + end + end + + def simplified_type(field_type) + return :integer if field_type =~ /^(big|)serial/i + return :string if field_type =~ /\[\]$/i || field_type =~ /^interval/i + return :string if field_type =~ /^(?:point|lseg|box|"?path"?|polygon|circle)/i + return :datetime if field_type =~ /^timestamp/i + return :float if field_type =~ /^(?:real|double precision)$/i + return :binary if field_type =~ /^bytea/i + return :boolean if field_type =~ /^bool/i + return :decimal if field_type == 'numeric(131089)' + super + end + + def cast_to_boolean(value) + return nil if value.nil? + if value == true || value == false + value + else + %w(true t 1).include?(value.to_s.downcase) + end + end + + # Post process default value from JDBC into a Rails-friendly format (columns{-internal}) + def default_value(value) + # Boolean types + return "t" if value =~ /true/i + return "f" if value =~ /false/i + + # Char/String/Bytea type values + return $1 if value =~ /^'(.*)'::(bpchar|text|character varying|bytea)$/ + + # Numeric values + return value.delete("()") if value =~ /^\(?-?[0-9]+(\.[0-9]*)?\)?/ + + # Fixed dates / timestamp + return $1 if value =~ /^'(.+)'::(date|timestamp)/ + + # Anything else is blank, some user type, or some function + # and we can't know the value of that, so return nil. + return nil + end + end + + def modify_types(tp) + tp[:primary_key] = "serial primary key" + + # sonar + # tp[:string][:limit] = 255 + # /sonar + + tp[:integer][:limit] = nil + tp[:boolean] = { :name => "boolean" } + tp[:float] = { :name => "float" } + tp[:text] = { :name => "text" } + tp[:datetime] = { :name => "timestamp" } + tp[:timestamp] = { :name => "timestamp" } + tp[:time] = { :name => "time" } + tp[:date] = { :name => "date" } + tp[:decimal] = { :name => "decimal" } + + # sonar + # New type + tp[:big_integer] = { :name => "int8", :limit => nil } + # /sonar + + tp + end + + def adapter_name #:nodoc: + 'PostgreSQL' + end + + def arel2_visitors + {'jdbcpostgresql' => ::Arel::Visitors::PostgreSQL} + end + + def postgresql_version + @postgresql_version ||= + begin + value = select_value('SELECT version()') + if value =~ /PostgreSQL (\d+)\.(\d+)\.(\d+)/ + ($1.to_i * 10000) + ($2.to_i * 100) + $3.to_i + else + 0 + end + end + end + + # Does PostgreSQL support migrations? + def supports_migrations? + true + end + + # Does PostgreSQL support standard conforming strings? + def supports_standard_conforming_strings? + # Temporarily set the client message level above error to prevent unintentional + # error messages in the logs when working on a PostgreSQL database server that + # does not support standard conforming strings. + client_min_messages_old = client_min_messages + self.client_min_messages = 'panic' + + # postgres-pr does not raise an exception when client_min_messages is set higher + # than error and "SHOW standard_conforming_strings" fails, but returns an empty + # PGresult instead. + has_support = select('SHOW standard_conforming_strings').to_a[0][0] rescue false + self.client_min_messages = client_min_messages_old + has_support + end + + def supports_insert_with_returning? + postgresql_version >= 80200 + end + + def supports_ddl_transactions? + false + end + + def supports_savepoints? + true + end + + def supports_count_distinct? #:nodoc: + false + end + + def create_savepoint + execute("SAVEPOINT #{current_savepoint_name}") + end + + def rollback_to_savepoint + execute("ROLLBACK TO SAVEPOINT #{current_savepoint_name}") + end + + def release_savepoint + execute("RELEASE SAVEPOINT #{current_savepoint_name}") + end + + # Returns the configured supported identifier length supported by PostgreSQL, + # or report the default of 63 on PostgreSQL 7.x. + def table_alias_length + @table_alias_length ||= (postgresql_version >= 80000 ? select_one('SHOW max_identifier_length')['max_identifier_length'].to_i : 63) + end + + def default_sequence_name(table_name, pk = nil) + default_pk, default_seq = pk_and_sequence_for(table_name) + default_seq || "#{table_name}_#{pk || default_pk || 'id'}_seq" + end + + # Resets sequence to the max value of the table's pk if present. + def reset_pk_sequence!(table, pk = nil, sequence = nil) #:nodoc: + unless pk and sequence + default_pk, default_sequence = pk_and_sequence_for(table) + pk ||= default_pk + sequence ||= default_sequence + end + if pk + if sequence + quoted_sequence = quote_column_name(sequence) + + select_value <<-end_sql, 'Reset sequence' + SELECT setval('#{quoted_sequence}', (SELECT COALESCE(MAX(#{quote_column_name pk})+(SELECT increment_by FROM #{quoted_sequence}), (SELECT min_value FROM #{quoted_sequence})) FROM #{quote_table_name(table)}), false) + end_sql + else + @logger.warn "#{table} has primary key #{pk} with no default sequence" if @logger + end + end + end + + # Find a table's primary key and sequence. + def pk_and_sequence_for(table) #:nodoc: + # First try looking for a sequence with a dependency on the + # given table's primary key. + result = select(<<-end_sql, 'PK and serial sequence')[0] + SELECT attr.attname, seq.relname + FROM pg_class seq, + pg_attribute attr, + pg_depend dep, + pg_namespace name, + pg_constraint cons + WHERE seq.oid = dep.objid + AND seq.relkind = 'S' + AND attr.attrelid = dep.refobjid + AND attr.attnum = dep.refobjsubid + AND attr.attrelid = cons.conrelid + AND attr.attnum = cons.conkey[1] + AND cons.contype = 'p' + AND dep.refobjid = '#{quote_table_name(table)}'::regclass + end_sql + + if result.nil? or result.empty? + # If that fails, try parsing the primary key's default value. + # Support the 7.x and 8.0 nextval('foo'::text) as well as + # the 8.1+ nextval('foo'::regclass). + result = select(<<-end_sql, 'PK and custom sequence')[0] + SELECT attr.attname, + CASE + WHEN split_part(def.adsrc, '''', 2) ~ '.' THEN + substr(split_part(def.adsrc, '''', 2), + strpos(split_part(def.adsrc, '''', 2), '.')+1) + ELSE split_part(def.adsrc, '''', 2) + END as relname + FROM pg_class t + JOIN pg_attribute attr ON (t.oid = attrelid) + JOIN pg_attrdef def ON (adrelid = attrelid AND adnum = attnum) + JOIN pg_constraint cons ON (conrelid = adrelid AND adnum = conkey[1]) + WHERE t.oid = '#{quote_table_name(table)}'::regclass + AND cons.contype = 'p' + AND def.adsrc ~* 'nextval' + end_sql + end + + [result["attname"], result["relname"]] + rescue + nil + end + + def pg_insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) + # Extract the table from the insert sql. Yuck. + table = sql.split(" ", 4)[2].gsub('"', '') + + # Try an insert with 'returning id' if available (PG >= 8.2) + if supports_insert_with_returning? && id_value.nil? + pk, sequence_name = *pk_and_sequence_for(table) unless pk + if pk + id_value = select_value("#{sql} RETURNING #{quote_column_name(pk)}") + clear_query_cache #FIXME: Why now? + return id_value + end + end + + # Otherwise, plain insert + execute(sql, name) + + # Don't need to look up id_value if we already have it. + # (and can't in case of non-sequence PK) + unless id_value + # If neither pk nor sequence name is given, look them up. + unless pk || sequence_name + pk, sequence_name = *pk_and_sequence_for(table) + end + + # If a pk is given, fallback to default sequence name. + # Don't fetch last insert id for a table without a pk. + if pk && sequence_name ||= default_sequence_name(table, pk) + id_value = last_insert_id(table, sequence_name) + end + end + id_value + end + + def pg_columns(table_name, name=nil) + schema_name = @config[:schema_search_path] + if table_name =~ /\./ + parts = table_name.split(/\./) + table_name = parts.pop + schema_name = parts.join(".") + end + schema_list = if schema_name.nil? + [] + else + schema_name.split(/\s*,\s*/) + end + while schema_list.size > 1 + s = schema_list.shift + begin + return @connection.columns_internal(table_name, name, s) + rescue ActiveRecord::JDBCError=>ignored_for_next_schema + end + end + s = schema_list.shift + return @connection.columns_internal(table_name, name, s) + end + + # From postgresql_adapter.rb + def indexes(table_name, name = nil) + result = select_rows(<<-SQL, name) + SELECT i.relname, d.indisunique, a.attname + FROM pg_class t, pg_class i, pg_index d, pg_attribute a + WHERE i.relkind = 'i' + AND d.indexrelid = i.oid + AND d.indisprimary = 'f' + AND t.oid = d.indrelid + AND t.relname = '#{table_name}' + AND a.attrelid = t.oid + AND ( d.indkey[0]=a.attnum OR d.indkey[1]=a.attnum + OR d.indkey[2]=a.attnum OR d.indkey[3]=a.attnum + OR d.indkey[4]=a.attnum OR d.indkey[5]=a.attnum + OR d.indkey[6]=a.attnum OR d.indkey[7]=a.attnum + OR d.indkey[8]=a.attnum OR d.indkey[9]=a.attnum ) + ORDER BY i.relname + SQL + + current_index = nil + indexes = [] + + result.each do |row| + if current_index != row[0] + indexes << ::ActiveRecord::ConnectionAdapters::IndexDefinition.new(table_name, row[0], row[1] == "t", []) + current_index = row[0] + end + + indexes.last.columns << row[2] + end + + indexes + end + + def last_insert_id(table, sequence_name) + Integer(select_value("SELECT currval('#{sequence_name}')")) + end + + def recreate_database(name) + drop_database(name) + create_database(name) + end + + def create_database(name, options = {}) + execute "CREATE DATABASE \"#{name}\" ENCODING='#{options[:encoding] || 'utf8'}'" + end + + def drop_database(name) + execute "DROP DATABASE IF EXISTS \"#{name}\"" + end + + def create_schema(schema_name, pg_username) + execute("CREATE SCHEMA \"#{schema_name}\" AUTHORIZATION \"#{pg_username}\"") + end + + def drop_schema(schema_name) + execute("DROP SCHEMA \"#{schema_name}\"") + end + + def all_schemas + select('select nspname from pg_namespace').map {|r| r["nspname"] } + end + + def primary_key(table) + pk_and_sequence = pk_and_sequence_for(table) + pk_and_sequence && pk_and_sequence.first + end + + def structure_dump + database = @config[:database] + if database.nil? + if @config[:url] =~ /\/([^\/]*)$/ + database = $1 + else + raise "Could not figure out what database this url is for #{@config["url"]}" + end + end + + ENV['PGHOST'] = @config[:host] if @config[:host] + ENV['PGPORT'] = @config[:port].to_s if @config[:port] + ENV['PGPASSWORD'] = @config[:password].to_s if @config[:password] + search_path = @config[:schema_search_path] + search_path = "--schema=#{search_path}" if search_path + + @connection.connection.close + begin + definition = `pg_dump -i -U "#{@config[:username]}" -s -x -O #{search_path} #{database}` + raise "Error dumping database" if $?.exitstatus == 1 + + # need to patch away any references to SQL_ASCII as it breaks the JDBC driver + definition.gsub(/SQL_ASCII/, 'UNICODE') + ensure + reconnect! + end + end + + # SELECT DISTINCT clause for a given set of columns and a given ORDER BY clause. + # + # PostgreSQL requires the ORDER BY columns in the select list for distinct queries, and + # requires that the ORDER BY include the distinct column. + # + # distinct("posts.id", "posts.created_at desc") + def distinct(columns, order_by) + return "DISTINCT #{columns}" if order_by.blank? + + # construct a clean list of column names from the ORDER BY clause, removing + # any asc/desc modifiers + order_columns = order_by.split(',').collect { |s| s.split.first } + order_columns.delete_if(&:blank?) + order_columns = order_columns.zip((0...order_columns.size).to_a).map { |s,i| "#{s} AS alias_#{i}" } + + # return a DISTINCT ON() clause that's distinct on the columns we want but includes + # all the required columns for the ORDER BY to work properly + sql = "DISTINCT ON (#{columns}) #{columns}, " + sql << order_columns * ', ' + end + + # ORDER BY clause for the passed order option. + # + # PostgreSQL does not allow arbitrary ordering when using DISTINCT ON, so we work around this + # by wrapping the sql as a sub-select and ordering in that query. + def add_order_by_for_association_limiting!(sql, options) + return sql if options[:order].blank? + + order = options[:order].split(',').collect { |s| s.strip }.reject(&:blank?) + order.map! { |s| 'DESC' if s =~ /\bdesc$/i } + order = order.zip((0...order.size).to_a).map { |s,i| "id_list.alias_#{i} #{s}" }.join(', ') + + sql.replace "SELECT * FROM (#{sql}) AS id_list ORDER BY #{order}" + end + + def quote(value, column = nil) #:nodoc: + return super unless column + + if value.kind_of?(String) && column.type == :binary + "E'#{escape_bytea(value)}'" + elsif value.kind_of?(String) && column.sql_type == 'xml' + "xml '#{quote_string(value)}'" + elsif value.kind_of?(Numeric) && column.sql_type == 'money' + # Not truly string input, so doesn't require (or allow) escape string syntax. + "'#{value}'" + elsif value.kind_of?(String) && column.sql_type =~ /^bit/ + case value + when /^[01]*$/ + "B'#{value}'" # Bit-string notation + when /^[0-9A-F]*$/i + "X'#{value}'" # Hexadecimal notation + end + else + super + end + end + + def escape_bytea(s) + if s + result = '' + s.each_byte { |c| result << sprintf('\\\\%03o', c) } + result + end + end + + def quote_table_name(name) + schema, name_part = extract_pg_identifier_from_name(name.to_s) + + unless name_part + quote_column_name(schema) + else + table_name, name_part = extract_pg_identifier_from_name(name_part) + "#{quote_column_name(schema)}.#{quote_column_name(table_name)}" + end + end + + def quote_column_name(name) + %("#{name}") + end + + def quoted_date(value) #:nodoc: + if value.acts_like?(:time) && value.respond_to?(:usec) + "#{super}.#{sprintf("%06d", value.usec)}" + else + super + end + end + + def disable_referential_integrity(&block) #:nodoc: + execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} DISABLE TRIGGER ALL" }.join(";")) + yield + ensure + execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} ENABLE TRIGGER ALL" }.join(";")) + end + + def rename_table(name, new_name) + execute "ALTER TABLE #{name} RENAME TO #{new_name}" + end + + # Adds a new column to the named table. + # See TableDefinition#column for details of the options you can use. + def add_column(table_name, column_name, type, options = {}) + default = options[:default] + notnull = options[:null] == false + + # Add the column. + execute("ALTER TABLE #{quote_table_name(table_name)} ADD COLUMN #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}") + + change_column_default(table_name, column_name, default) if options_include_default?(options) + change_column_null(table_name, column_name, false, default) if notnull + end + + # Changes the column of a table. + def change_column(table_name, column_name, type, options = {}) + quoted_table_name = quote_table_name(table_name) + + begin + execute "ALTER TABLE #{quoted_table_name} ALTER COLUMN #{quote_column_name(column_name)} TYPE #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}" + rescue ActiveRecord::StatementInvalid => e + raise e if postgresql_version > 80000 + # This is PostgreSQL 7.x, so we have to use a more arcane way of doing it. + begin + begin_db_transaction + tmp_column_name = "#{column_name}_ar_tmp" + add_column(table_name, tmp_column_name, type, options) + execute "UPDATE #{quoted_table_name} SET #{quote_column_name(tmp_column_name)} = CAST(#{quote_column_name(column_name)} AS #{type_to_sql(type, options[:limit], options[:precision], options[:scale])})" + remove_column(table_name, column_name) + rename_column(table_name, tmp_column_name, column_name) + commit_db_transaction + rescue + rollback_db_transaction + end + end + + change_column_default(table_name, column_name, options[:default]) if options_include_default?(options) + change_column_null(table_name, column_name, options[:null], options[:default]) if options.key?(:null) + end + + # Changes the default value of a table column. + def change_column_default(table_name, column_name, default) + execute "ALTER TABLE #{quote_table_name(table_name)} ALTER COLUMN #{quote_column_name(column_name)} SET DEFAULT #{quote(default)}" + end + + def change_column_null(table_name, column_name, null, default = nil) + unless null || default.nil? + execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL") + end + execute("ALTER TABLE #{quote_table_name(table_name)} ALTER #{quote_column_name(column_name)} #{null ? 'DROP' : 'SET'} NOT NULL") + end + + def rename_column(table_name, column_name, new_column_name) #:nodoc: + execute "ALTER TABLE #{quote_table_name(table_name)} RENAME COLUMN #{quote_column_name(column_name)} TO #{quote_column_name(new_column_name)}" + end + + def remove_index(table_name, options) #:nodoc: + execute "DROP INDEX #{index_name(table_name, options)}" + end + + def type_to_sql(type, limit = nil, precision = nil, scale = nil) #:nodoc: + return super unless type.to_s == 'integer' + + if limit.nil? || limit == 4 + 'integer' + elsif limit < 4 + 'smallint' + else + 'bigint' + end + end + + def tables + @connection.tables(database_name, nil, nil, ["TABLE"]) + end + + private + def translate_exception(exception, message) + case exception.message + when /duplicate key value violates unique constraint/ + ::ActiveRecord::RecordNotUnique.new(message, exception) + when /violates foreign key constraint/ + ::ActiveRecord::InvalidForeignKey.new(message, exception) + else + super + end + end + + def extract_pg_identifier_from_name(name) + match_data = name[0,1] == '"' ? name.match(/\"([^\"]+)\"/) : name.match(/([^\.]+)/) + + if match_data + rest = name[match_data[0].length..-1] + rest = rest[1..-1] if rest[0,1] == "." + [match_data[1], (rest.length > 0 ? rest : nil)] + end + end + end +end + diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/postgresql/connection_methods.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/postgresql/connection_methods.rb new file mode 100644 index 00000000000..715a63ae7c8 --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/postgresql/connection_methods.rb @@ -0,0 +1,21 @@ +# Don't need to load native postgres adapter +$LOADED_FEATURES << "active_record/connection_adapters/postgresql_adapter.rb" + +class ActiveRecord::Base + class << self + def postgresql_connection(config) + require "arjdbc/postgresql" + config[:host] ||= "localhost" + config[:port] ||= 5432 + config[:url] ||= "jdbc:postgresql://#{config[:host]}:#{config[:port]}/#{config[:database]}" + config[:url] << config[:pg_params] if config[:pg_params] + config[:driver] ||= "org.postgresql.Driver" + conn = jdbc_connection(config) + conn.execute("SET SEARCH_PATH TO #{config[:schema_search_path]}") if config[:schema_search_path] + conn + end + alias_method :jdbcpostgresql_connection, :postgresql_connection + end +end + + diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/sqlite3.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/sqlite3.rb new file mode 100644 index 00000000000..1d60cda2ef4 --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/sqlite3.rb @@ -0,0 +1,4 @@ +require 'arjdbc/jdbc' +jdbc_require_driver 'jdbc/sqlite3' +require 'arjdbc/sqlite3/connection_methods' +require 'arjdbc/sqlite3/adapter' diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/sqlite3/adapter.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/sqlite3/adapter.rb new file mode 100644 index 00000000000..c3556060388 --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/sqlite3/adapter.rb @@ -0,0 +1,381 @@ +require 'arjdbc/jdbc/missing_functionality_helper' + +module ActiveRecord::ConnectionAdapters + Sqlite3Adapter = Class.new(AbstractAdapter) unless const_defined?(:Sqlite3Adapter) +end + +module ::ArJdbc + module SQLite3 + def self.column_selector + [/sqlite/i, lambda {|cfg,col| col.extend(::ArJdbc::SQLite3::Column)}] + end + + def self.jdbc_connection_class + ::ActiveRecord::ConnectionAdapters::Sqlite3JdbcConnection + end + + module Column + def init_column(name, default, *args) + @default = '' if default =~ /NULL/ + end + + def type_cast(value) + return nil if value.nil? + case type + when :string then value + when :primary_key then defined?(value.to_i) ? value.to_i : (value ? 1 : 0) + when :float then value.to_f + when :decimal then self.class.value_to_decimal(value) + when :boolean then self.class.value_to_boolean(value) + else super + end + end + + private + def simplified_type(field_type) + case field_type + when /boolean/i then :boolean + when /text/i then :text + when /varchar/i then :string + when /int/i then :integer + when /float/i then :float + when /real|decimal/i then @scale == 0 ? :integer : :decimal + when /datetime/i then :datetime + when /date/i then :date + when /time/i then :time + when /blob/i then :binary + end + end + + def extract_limit(sql_type) + return nil if sql_type =~ /^(real)\(\d+/i + super + end + + def extract_precision(sql_type) + case sql_type + when /^(real)\((\d+)(,\d+)?\)/i then $2.to_i + else super + end + end + + def extract_scale(sql_type) + case sql_type + when /^(real)\((\d+)\)/i then 0 + when /^(real)\((\d+)(,(\d+))\)/i then $4.to_i + else super + end + end + + # Post process default value from JDBC into a Rails-friendly format (columns{-internal}) + def default_value(value) + # jdbc returns column default strings with actual single quotes around the value. + return $1 if value =~ /^'(.*)'$/ + + value + end + end + + def adapter_name #:nodoc: + 'SQLite' + end + + def arel2_visitors + {'jdbcsqlite3' => ::Arel::Visitors::SQLite} + end + + def supports_ddl_transactions? + true # sqlite_version >= '2.0.0' + end + + def supports_add_column? + sqlite_version >= '3.1.6' + end + + def supports_count_distinct? #:nodoc: + sqlite_version >= '3.2.6' + end + + def supports_autoincrement? #:nodoc: + sqlite_version >= '3.1.0' + end + + def sqlite_version + @sqlite_version ||= select_value('select sqlite_version(*)') + end + + def modify_types(tp) + tp[:primary_key] = "integer primary key autoincrement not null" + tp[:string] = { :name => "varchar", :limit => 255 } + tp[:text] = { :name => "text" } + tp[:float] = { :name => "float" } + tp[:decimal] = { :name => "decimal" } + tp[:datetime] = { :name => "datetime" } + tp[:timestamp] = { :name => "datetime" } + tp[:time] = { :name => "time" } + tp[:date] = { :name => "date" } + tp[:boolean] = { :name => "boolean" } + tp[:binary] = { :name => "blob" } + tp + end + + def quote_column_name(name) #:nodoc: + %Q("#{name}") + end + + def quote_string(str) + str.gsub(/'/, "''") + end + + def quoted_true + %Q{'t'} + end + + def quoted_false + %Q{'f'} + end + + # Quote date/time values for use in SQL input. Includes microseconds + # if the value is a Time responding to usec. + def quoted_date(value) #:nodoc: + if value.respond_to?(:usec) + "#{super}.#{sprintf("%06d", value.usec)}" + else + super + end + end + + def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) #:nodoc: + @connection.execute_update(sql) + id_value || last_insert_id + end + + def last_insert_id + Integer(select_value("SELECT last_insert_rowid()")) + end + + def tables(name = nil) #:nodoc: + sql = <<-SQL + SELECT name + FROM sqlite_master + WHERE type = 'table' AND NOT name = 'sqlite_sequence' + SQL + + select_rows(sql, name).map do |row| + row[0] + end + end + + def indexes(table_name, name = nil) + result = select_rows("SELECT name, sql FROM sqlite_master WHERE tbl_name = #{quote_table_name(table_name)} AND type = 'index'", name) + + result.collect do |row| + name = row[0] + index_sql = row[1] + unique = (index_sql =~ /unique/i) + cols = index_sql.match(/\((.*)\)/)[1].gsub(/,/,' ').split.map do |c| + match = /^"(.+)"$/.match(c); match ? match[1] : c + end + ::ActiveRecord::ConnectionAdapters::IndexDefinition.new(table_name, name, unique, cols) + end + end + + def primary_key(table_name) #:nodoc: + column = table_structure(table_name).find {|field| field['pk'].to_i == 1} + column ? column['name'] : nil + end + + def recreate_database(name) + tables.each{ |table| drop_table(table) } + end + + def _execute(sql, name = nil) + result = super + ActiveRecord::ConnectionAdapters::JdbcConnection::insert?(sql) ? last_insert_id : result + end + + def select(sql, name=nil) + execute(sql, name).map do |row| + record = {} + row.each_key do |key| + if key.is_a?(String) + record[key.sub(/^"?\w+"?\./, '')] = row[key] + end + end + record + end + end + + def table_structure(table_name) + structure = @connection.execute_query("PRAGMA table_info(#{quote_table_name(table_name)})") + raise ActiveRecord::StatementInvalid, "Could not find table '#{table_name}'" if structure.empty? + structure + end + + def jdbc_columns(table_name, name = nil) #:nodoc: + table_structure(table_name).map do |field| + ::ActiveRecord::ConnectionAdapters::SQLite3Column.new(@config, field['name'], field['dflt_value'], field['type'], field['notnull'] == 0) + end + end + + def primary_key(table_name) #:nodoc: + column = table_structure(table_name).find { |field| + field['pk'].to_i == 1 + } + column && column['name'] + end + + def remove_index!(table_name, index_name) #:nodoc: + execute "DROP INDEX #{quote_column_name(index_name)}" + end + + def rename_table(name, new_name) + execute "ALTER TABLE #{quote_table_name(name)} RENAME TO #{quote_table_name(new_name)}" + end + + # See: http://www.sqlite.org/lang_altertable.html + # SQLite has an additional restriction on the ALTER TABLE statement + def valid_alter_table_options( type, options) + type.to_sym != :primary_key + end + + def add_column(table_name, column_name, type, options = {}) #:nodoc: + if supports_add_column? && valid_alter_table_options( type, options ) + super(table_name, column_name, type, options) + else + alter_table(table_name) do |definition| + definition.column(column_name, type, options) + end + end + end + + def remove_column(table_name, *column_names) #:nodoc: + raise ArgumentError.new("You must specify at least one column name. Example: remove_column(:people, :first_name)") if column_names.empty? + column_names.flatten.each do |column_name| + alter_table(table_name) do |definition| + definition.columns.delete(definition[column_name]) + end + end + end + alias :remove_columns :remove_column + + def change_column_default(table_name, column_name, default) #:nodoc: + alter_table(table_name) do |definition| + definition[column_name].default = default + end + end + + def change_column_null(table_name, column_name, null, default = nil) + unless null || default.nil? + execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL") + end + alter_table(table_name) do |definition| + definition[column_name].null = null + end + end + + def change_column(table_name, column_name, type, options = {}) #:nodoc: + alter_table(table_name) do |definition| + include_default = options_include_default?(options) + definition[column_name].instance_eval do + self.type = type + self.limit = options[:limit] if options.include?(:limit) + self.default = options[:default] if include_default + self.null = options[:null] if options.include?(:null) + end + end + end + + def rename_column(table_name, column_name, new_column_name) #:nodoc: + unless columns(table_name).detect{|c| c.name == column_name.to_s } + raise ActiveRecord::ActiveRecordError, "Missing column #{table_name}.#{column_name}" + end + alter_table(table_name, :rename => {column_name.to_s => new_column_name.to_s}) + end + + # SELECT ... FOR UPDATE is redundant since the table is locked. + def add_lock!(sql, options) #:nodoc: + sql + end + + def empty_insert_statement_value + "VALUES(NULL)" + end + + protected + include ArJdbc::MissingFunctionalityHelper + + def translate_exception(exception, message) + case exception.message + when /column(s)? .* (is|are) not unique/ + ActiveRecord::RecordNotUnique.new(message, exception) + else + super + end + end + end +end + +module ActiveRecord::ConnectionAdapters + remove_const(:SQLite3Adapter) if const_defined?(:SQLite3Adapter) + remove_const(:SQLiteAdapter) if const_defined?(:SQLiteAdapter) + + class SQLite3Column < JdbcColumn + include ArJdbc::SQLite3::Column + + def initialize(name, *args) + if Hash === name + super + else + super(nil, name, *args) + end + end + + def call_discovered_column_callbacks(*) + end + + def self.string_to_binary(value) + "\000b64" + [value].pack('m*').split("\n").join('') + end + + def self.binary_to_string(value) + if value.respond_to?(:force_encoding) && value.encoding != Encoding::ASCII_8BIT + value = value.force_encoding(Encoding::ASCII_8BIT) + end + + if value[0..3] == "\000b64" + value[4..-1].unpack('m*').first + else + value + end + end + end + + class SQLite3Adapter < JdbcAdapter + include ArJdbc::SQLite3 + + def adapter_spec(config) + # return nil to avoid extending ArJdbc::SQLite3, which we've already done + end + + def jdbc_connection_class(spec) + ::ArJdbc::SQLite3.jdbc_connection_class + end + + def jdbc_column_class + ActiveRecord::ConnectionAdapters::SQLite3Column + end + + alias_chained_method :columns, :query_cache, :jdbc_columns + end + + SQLiteAdapter = SQLite3Adapter +end + +# Fake out sqlite3/version driver for AR tests +$LOADED_FEATURES << 'sqlite3/version.rb' +module SQLite3 + module Version + VERSION = '1.2.6' # query_cache_test.rb requires SQLite3::Version::VERSION > '1.2.5' + end +end diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/sqlite3/connection_methods.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/sqlite3/connection_methods.rb new file mode 100644 index 00000000000..8e4d9769f55 --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/sqlite3/connection_methods.rb @@ -0,0 +1,34 @@ +# Don't need to load native sqlite3 adapter +$LOADED_FEATURES << "active_record/connection_adapters/sqlite_adapter.rb" +$LOADED_FEATURES << "active_record/connection_adapters/sqlite3_adapter.rb" + +class ActiveRecord::Base + class << self + def sqlite3_connection(config) + require "arjdbc/sqlite3" + + parse_sqlite3_config!(config) + database = config[:database] + database = '' if database == ':memory:' + config[:url] ||= "jdbc:sqlite:#{database}" + config[:driver] ||= "org.sqlite.JDBC" + config[:adapter_class] = ActiveRecord::ConnectionAdapters::SQLite3Adapter + jdbc_connection(config) + end + + def parse_sqlite3_config!(config) + config[:database] ||= config[:dbfile] + + # Allow database path relative to RAILS_ROOT, but only if + # the database path is not the special path that tells + # Sqlite to build a database only in memory. + rails_root_defined = defined?(Rails.root) || Object.const_defined?(:RAILS_ROOT) + if rails_root_defined && ':memory:' != config[:database] + rails_root = defined?(Rails.root) ? Rails.root : RAILS_ROOT + config[:database] = File.expand_path(config[:database], rails_root) + end + end + + alias_method :jdbcsqlite3_connection, :sqlite3_connection + end +end diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/sybase.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/sybase.rb new file mode 100644 index 00000000000..86b3b40e56c --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/sybase.rb @@ -0,0 +1,2 @@ +require 'arjdbc/jdbc' +require 'arjdbc/sybase/adapter.rb' diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/sybase/adapter.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/sybase/adapter.rb new file mode 100644 index 00000000000..02096a4cdb6 --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/sybase/adapter.rb @@ -0,0 +1,46 @@ +module ArJdbc + module Sybase + def add_limit_offset!(sql, options) # :nodoc: + @limit = options[:limit] + @offset = options[:offset] + if use_temp_table? + # Use temp table to hack offset with Sybase + sql.sub!(/ FROM /i, ' INTO #artemp FROM ') + elsif zero_limit? + # "SET ROWCOUNT 0" turns off limits, so we havesy + # to use a cheap trick. + if sql =~ /WHERE/i + sql.sub!(/WHERE/i, 'WHERE 1 = 2 AND ') + elsif sql =~ /ORDER\s+BY/i + sql.sub!(/ORDER\s+BY/i, 'WHERE 1 = 2 ORDER BY') + else + sql << 'WHERE 1 = 2' + end + end + end + + # If limit is not set at all, we can ignore offset; + # if limit *is* set but offset is zero, use normal select + # with simple SET ROWCOUNT. Thus, only use the temp table + # if limit is set and offset > 0. + def use_temp_table? + !@limit.nil? && !@offset.nil? && @offset > 0 + end + + def zero_limit? + !@limit.nil? && @limit == 0 + end + + def modify_types(tp) #:nodoc: + tp[:primary_key] = "NUMERIC(22,0) IDENTITY PRIMARY KEY" + tp[:integer][:limit] = nil + tp[:boolean] = {:name => "bit"} + tp[:binary] = {:name => "image"} + tp + end + + def remove_index(table_name, options = {}) + execute "DROP INDEX #{table_name}.#{index_name(table_name, options)}" + end + end +end diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/version.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/version.rb new file mode 100644 index 00000000000..503ef20d4fb --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/arjdbc/version.rb @@ -0,0 +1,8 @@ +module ArJdbc + module Version + VERSION = "1.1.3" + end +end +# Compatibility with older versions of ar-jdbc for other extensions out there +JdbcAdapter = ArJdbc +JdbcSpec = ArJdbc diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/generators/jdbc/jdbc_generator.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/generators/jdbc/jdbc_generator.rb new file mode 100644 index 00000000000..372b640e0dd --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/generators/jdbc/jdbc_generator.rb @@ -0,0 +1,9 @@ +class JdbcGenerator < Rails::Generators::Base + def self.source_root + @source_root ||= File.expand_path('../../../../rails_generators/templates', __FILE__) + end + + def create_jdbc_files + directory '.', '.' + end +end diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/jdbc_adapter.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/jdbc_adapter.rb new file mode 100644 index 00000000000..29f503be77e --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/jdbc_adapter.rb @@ -0,0 +1,2 @@ +warn "DEPRECATED: require 'arjdbc' instead of 'jdbc_adapter'." +require 'arjdbc' diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/jdbc_adapter/rake_tasks.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/jdbc_adapter/rake_tasks.rb new file mode 100644 index 00000000000..b95ebcd3f26 --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/jdbc_adapter/rake_tasks.rb @@ -0,0 +1,3 @@ +warn "DEPRECATED: require 'arjdbc/rake_tasks' instead of 'jdbc_adapter/rake_tasks'." +require 'arjdbc/jdbc/rake_tasks' + diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/jdbc_adapter/version.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/jdbc_adapter/version.rb new file mode 100644 index 00000000000..5ed33f15bc2 --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/jdbc_adapter/version.rb @@ -0,0 +1,3 @@ +warn "DEPRECATED: require 'arjdbc/version' instead of 'jdbc_adapter/version'." +require 'arjdbc/version' + diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/pg.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/pg.rb new file mode 100644 index 00000000000..4a7759d56ae --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-jdbc-adapter-1.1.3/lib/pg.rb @@ -0,0 +1,26 @@ +# Stub library for postgresql -- allows Rails to load +# postgresql_adapter without error. Other than postgres-pr, there's no +# other way to use PostgreSQL on JRuby anyway, right? If you've +# installed ar-jdbc you probably want to use that to connect to pg. +# +# If by chance this library is installed in another Ruby and this file +# got required then we'll just continue to try to load the next pg.rb +# in the $LOAD_PATH. + +unless defined?(JRUBY_VERSION) + gem 'pg' if respond_to?(:gem) # make sure pg gem is activated + after_current_file = false + $LOAD_PATH.each do |p| + require_file = File.join(p, 'pg.rb') + + if File.expand_path(require_file) == File.expand_path(__FILE__) + after_current_file = true + next + end + + if after_current_file && File.exist?(require_file) + load require_file + break + end + end +end diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-oracle_enhanced-adapter-1.4.0/.rspec b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-oracle_enhanced-adapter-1.4.0/.rspec new file mode 100644 index 00000000000..a5faa1d6a36 --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-oracle_enhanced-adapter-1.4.0/.rspec @@ -0,0 +1,2 @@ +--color +--backtrace diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-oracle_enhanced-adapter-1.4.0/.specification b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-oracle_enhanced-adapter-1.4.0/.specification new file mode 100644 index 00000000000..df761461fef --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-oracle_enhanced-adapter-1.4.0/.specification @@ -0,0 +1,230 @@ +--- !ruby/object:Gem::Specification +name: activerecord-oracle_enhanced-adapter +version: !ruby/object:Gem::Version + prerelease: + segments: + - 1 + - 4 + - 0 + version: 1.4.0 +platform: ruby +authors: + - Raimonds Simanovskis +autorequire: +bindir: bin +cert_chain: [] + +date: 2011-08-08 00:00:00 +02:00 +default_executable: +dependencies: + - !ruby/object:Gem::Dependency + name: jeweler + prerelease: false + requirement: &id001 !ruby/object:Gem::Requirement + none: false + requirements: + - - ~> + - !ruby/object:Gem::Version + version: 1.5.1 + type: :development + version_requirements: *id001 + - !ruby/object:Gem::Dependency + name: rspec + prerelease: false + requirement: &id002 !ruby/object:Gem::Requirement + none: false + requirements: + - - ~> + - !ruby/object:Gem::Version + version: "2.4" + type: :development + version_requirements: *id002 + - !ruby/object:Gem::Dependency + name: activerecord + prerelease: false + requirement: &id003 !ruby/object:Gem::Requirement + none: false + requirements: + - - ">=" + - !ruby/object:Gem::Version + version: "0" + type: :development + version_requirements: *id003 + - !ruby/object:Gem::Dependency + name: activemodel + prerelease: false + requirement: &id004 !ruby/object:Gem::Requirement + none: false + requirements: + - - ">=" + - !ruby/object:Gem::Version + version: "0" + type: :development + version_requirements: *id004 + - !ruby/object:Gem::Dependency + name: activesupport + prerelease: false + requirement: &id005 !ruby/object:Gem::Requirement + none: false + requirements: + - - ">=" + - !ruby/object:Gem::Version + version: "0" + type: :development + version_requirements: *id005 + - !ruby/object:Gem::Dependency + name: actionpack + prerelease: false + requirement: &id006 !ruby/object:Gem::Requirement + none: false + requirements: + - - ">=" + - !ruby/object:Gem::Version + version: "0" + type: :development + version_requirements: *id006 + - !ruby/object:Gem::Dependency + name: railties + prerelease: false + requirement: &id007 !ruby/object:Gem::Requirement + none: false + requirements: + - - ">=" + - !ruby/object:Gem::Version + version: "0" + type: :development + version_requirements: *id007 + - !ruby/object:Gem::Dependency + name: arel + prerelease: false + requirement: &id008 !ruby/object:Gem::Requirement + none: false + requirements: + - - ">=" + - !ruby/object:Gem::Version + version: "0" + type: :development + version_requirements: *id008 + - !ruby/object:Gem::Dependency + name: ruby-plsql + prerelease: false + requirement: &id009 !ruby/object:Gem::Requirement + none: false + requirements: + - - ">=" + - !ruby/object:Gem::Version + version: 0.4.4 + type: :development + version_requirements: *id009 + - !ruby/object:Gem::Dependency + name: ruby-oci8 + prerelease: false + requirement: &id010 !ruby/object:Gem::Requirement + none: false + requirements: + - - ~> + - !ruby/object:Gem::Version + version: 2.0.4 + type: :development + version_requirements: *id010 +description: | + Oracle "enhanced" ActiveRecord adapter contains useful additional methods for working with new and legacy Oracle databases. + This adapter is superset of original ActiveRecord Oracle adapter. + +email: raimonds.simanovskis@gmail.com +executables: [] + +extensions: [] + +extra_rdoc_files: + - README.md +files: + - .rspec + - Gemfile + - History.txt + - License.txt + - README.md + - RUNNING_TESTS.md + - Rakefile + - VERSION + - activerecord-oracle_enhanced-adapter.gemspec + - lib/active_record/connection_adapters/emulation/oracle_adapter.rb + - lib/active_record/connection_adapters/oracle_enhanced.rake + - lib/active_record/connection_adapters/oracle_enhanced_activerecord_patches.rb + - lib/active_record/connection_adapters/oracle_enhanced_adapter.rb + - lib/active_record/connection_adapters/oracle_enhanced_base_ext.rb + - lib/active_record/connection_adapters/oracle_enhanced_column.rb + - lib/active_record/connection_adapters/oracle_enhanced_connection.rb + - lib/active_record/connection_adapters/oracle_enhanced_context_index.rb + - lib/active_record/connection_adapters/oracle_enhanced_core_ext.rb + - lib/active_record/connection_adapters/oracle_enhanced_cpk.rb + - lib/active_record/connection_adapters/oracle_enhanced_dirty.rb + - lib/active_record/connection_adapters/oracle_enhanced_jdbc_connection.rb + - lib/active_record/connection_adapters/oracle_enhanced_oci_connection.rb + - lib/active_record/connection_adapters/oracle_enhanced_procedures.rb + - lib/active_record/connection_adapters/oracle_enhanced_schema_definitions.rb + - lib/active_record/connection_adapters/oracle_enhanced_schema_dumper.rb + - lib/active_record/connection_adapters/oracle_enhanced_schema_statements.rb + - lib/active_record/connection_adapters/oracle_enhanced_schema_statements_ext.rb + - lib/active_record/connection_adapters/oracle_enhanced_structure_dump.rb + - lib/active_record/connection_adapters/oracle_enhanced_tasks.rb + - lib/active_record/connection_adapters/oracle_enhanced_version.rb + - lib/activerecord-oracle_enhanced-adapter.rb + - spec/active_record/connection_adapters/oracle_enhanced_adapter_spec.rb + - spec/active_record/connection_adapters/oracle_enhanced_connection_spec.rb + - spec/active_record/connection_adapters/oracle_enhanced_context_index_spec.rb + - spec/active_record/connection_adapters/oracle_enhanced_core_ext_spec.rb + - spec/active_record/connection_adapters/oracle_enhanced_cpk_spec.rb + - spec/active_record/connection_adapters/oracle_enhanced_data_types_spec.rb + - spec/active_record/connection_adapters/oracle_enhanced_dbms_output_spec.rb + - spec/active_record/connection_adapters/oracle_enhanced_dirty_spec.rb + - spec/active_record/connection_adapters/oracle_enhanced_emulate_oracle_adapter_spec.rb + - spec/active_record/connection_adapters/oracle_enhanced_procedures_spec.rb + - spec/active_record/connection_adapters/oracle_enhanced_schema_dump_spec.rb + - spec/active_record/connection_adapters/oracle_enhanced_schema_statements_spec.rb + - spec/active_record/connection_adapters/oracle_enhanced_structure_dump_spec.rb + - spec/spec_helper.rb +has_rdoc: true +homepage: http://github.com/rsim/oracle-enhanced +licenses: [] + +post_install_message: +rdoc_options: [] + +require_paths: + - lib +required_ruby_version: !ruby/object:Gem::Requirement + none: false + requirements: + - - ">=" + - !ruby/object:Gem::Version + version: "0" +required_rubygems_version: !ruby/object:Gem::Requirement + none: false + requirements: + - - ">=" + - !ruby/object:Gem::Version + version: "0" +requirements: [] + +rubyforge_project: +rubygems_version: 1.5.1 +signing_key: +specification_version: 3 +summary: Oracle enhanced adapter for ActiveRecord +test_files: + - spec/active_record/connection_adapters/oracle_enhanced_adapter_spec.rb + - spec/active_record/connection_adapters/oracle_enhanced_connection_spec.rb + - spec/active_record/connection_adapters/oracle_enhanced_context_index_spec.rb + - spec/active_record/connection_adapters/oracle_enhanced_core_ext_spec.rb + - spec/active_record/connection_adapters/oracle_enhanced_cpk_spec.rb + - spec/active_record/connection_adapters/oracle_enhanced_data_types_spec.rb + - spec/active_record/connection_adapters/oracle_enhanced_dbms_output_spec.rb + - spec/active_record/connection_adapters/oracle_enhanced_dirty_spec.rb + - spec/active_record/connection_adapters/oracle_enhanced_emulate_oracle_adapter_spec.rb + - spec/active_record/connection_adapters/oracle_enhanced_procedures_spec.rb + - spec/active_record/connection_adapters/oracle_enhanced_schema_dump_spec.rb + - spec/active_record/connection_adapters/oracle_enhanced_schema_statements_spec.rb + - spec/active_record/connection_adapters/oracle_enhanced_structure_dump_spec.rb + - spec/spec_helper.rb + diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-oracle_enhanced-adapter-1.4.0/Gemfile b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-oracle_enhanced-adapter-1.4.0/Gemfile new file mode 100644 index 00000000000..0d9fe3c6391 --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-oracle_enhanced-adapter-1.4.0/Gemfile @@ -0,0 +1,40 @@ +source 'http://rubygems.org' + +group :development do + gem 'jeweler', '~> 1.5.1' + gem 'rspec', '~> 2.4' + + if ENV['RAILS_GEM_VERSION'] + gem 'activerecord', "=#{ENV['RAILS_GEM_VERSION']}" + gem 'actionpack', "=#{ENV['RAILS_GEM_VERSION']}" + gem 'activesupport', "=#{ENV['RAILS_GEM_VERSION']}" + case ENV['RAILS_GEM_VERSION'] + when /^2.0/ + gem 'composite_primary_keys', '=0.9.93' + when /^2.1/ + gem 'composite_primary_keys', '=1.0.8' + when /^2.2/ + gem 'composite_primary_keys', '=2.2.2' + when /^2.3.3/ + gem 'composite_primary_keys', '=2.3.2' + when /^3/ + gem 'railties', "=#{ENV['RAILS_GEM_VERSION']}" + end + else + # uses local copy of Rails 3 and Arel gems + ENV['RAILS_GEM_PATH'] ||= File.expand_path('../../rails', __FILE__) + %w(activerecord activemodel activesupport actionpack railties).each do |gem_name| + gem gem_name, :path => File.join(ENV['RAILS_GEM_PATH'], gem_name) + end + + ENV['AREL_GEM_PATH'] ||= File.expand_path('../../arel', __FILE__) + gem 'arel', :path => ENV['AREL_GEM_PATH'] + end + + gem 'ruby-plsql', '>=0.4.4' + + platforms :ruby do + gem 'ruby-oci8', '~> 2.0.4' + end + +end diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-oracle_enhanced-adapter-1.4.0/History.txt b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-oracle_enhanced-adapter-1.4.0/History.txt new file mode 100644 index 00000000000..cddf45d7e6e --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-oracle_enhanced-adapter-1.4.0/History.txt @@ -0,0 +1,252 @@ +== 1.4.0 2011-08-09 + +* Enhancements: + * Support for Rails 3.1 + * Bind parameter support for exec_insert, exec_update and exec_delete (in ActiveRecord 3.1) + * Purge recyclebin on rake db:test:purge + * Support transactional context index + * Require ojdbc6.jar (on Java 6) or ojdbc5.jar (on Java 5) JDBC drivers + * Support for RAW data type + * rake db:create and db:drop tasks + * Support virtual columns (in Oracle 11g) in schema dump + * It is possible to specify default tablespaces for tables, indexes, CLOBs and BLOBs + * rename_index migrations method + * Search for JDBC driver in ./lib directory of Rails application +* Bug fixes: + * Fixed context index dump when definition is larger than 4000 bytes + * Fixed schema dump not to conflict with other database adapters that are used in the same application + * Allow $ in table name prefix or suffix + +== 1.3.2 2011-01-05 + +* Enhancements: + * If no :host or :port is provided then connect with :database name (do not default :host to localhost) + * Database connection pool support for JRuby on Tomcat and JBoss application servers + * NLS connection parameters support via environment variables or database.yml + * Support for Arel 2.0 and latest Rails master branch + * Support for Rails 3.1 prepared statements (implemented in not yet released Rails master branch version) + * Eager loading of included association with more than 1000 records (implemented in not yet released Rails master branch version) +* Bug fixes: + * Foreign keys are added after table definitions in schema dump to ensure correct order of schema statements + * Quote NCHAR and NVARCHAR2 type values with N'...' + * Numeric username and/or password in database.yml will be automatically converted to string + +== 1.3.1 2010-09-09 + +* Enhancements: + * Tested with Rails 3.0.0 release + * Lexer options for context index creation + * Added Bundler for running adapter specs, added RUNNING_TESTS.rdoc with description how to run specs + * Connection to database using :host, :port and :database options + * Improved loading of adapter in Rails 3 using railtie +* Bug fixes: + * Fix for custom context index procedure when indexing records with null values + * Quote table and column names in write_lobs callback + * Fix for incorrect column SQL types when two models use the same table and AR query cache is enabled + * Fixes for schema and scructure dump tasks + * Fix for handling of zero-length strings in BLOB and CLOB columns + * removed String.mb_chars upcase and downcase methods for Ruby 1.9 as Rails 3.0.0 already includes Unicode aware upcase and downcase methods for Ruby 1.9 + * Fixes for latest ActiveRecord unit tests + +== 1.3.0 2010-06-21 + +* Enhancements: + * Rails 3.0.0.beta4 and Rails 2.3.x compatible + * When used with Rails 3 then works together with Oracle SQL compiler included in Arel gem (http://github.com/rails/arel) + * Rails 3: Better support for limit and offset (when possible adds just ROWNUM condition in WHERE clause without using subqueries) + * Table and column names are always quoted and in uppercase to avoid the need for checking Oracle reserved words + * Full text search index creation (add_context_index and remove_context_index methods in migrations and #contains method in ActiveRecord models) + * add_index and remove_index give just warnings on wrong index names (new expected behavior in Rails 2.3.8 and 3.0.0) + * :tablespace and :options options for create_table and add_index +* Workarounds: + * Rails 3: set ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.cache_columns = true in initializer file for all environments + (to avoid too many data dictionary queries from Arel) + * Rails 2.3: patch several ActiveRecord methods to work correctly with quoted table names in uppercase (see oracle_enhanced_activerecord_patches.rb). + These patches are already included in Rails 3.0.0.beta4. +* Bug fixes: + * Fixes for schema purge (drop correctly materialized views) + * Fixes for schema dump and structure dump (use correct statement separator) + * Only use Oracle specific schema dump for Oracle connections + +== 1.2.4 2010-02-23 + +* Enhancements: + * rake db:test:purge will drop all schema objects from test schema (including views, synonyms, packages, functions, procedures) - + they should be always reloaded before tests run if necessary + * added views, synonyms, packages, functions, procedures, indexes, triggers, types, primary, unique and foreign key constraints to structure dump + * added :temporary option for create_table to create temporary tables + * added :tablespace option for add_index + * support function based indexes in schema dump + * support JNDI database connections in JRuby + * check ruby-oci8 minimum version 2.0.3 + * added savepoints support (nested ActiveRecord transactions) +* Bug fixes: + * typecast returned BigDecimal integer values to Fixnum or Bignum + (to avoid issues with _before_type_cast values for id attributes because _before_type_cast is used in form helpers) + * clear table columns cache after columns definition change in migrations + +== 1.2.3 2009-12-09 + +* Enhancements + * support fractional seconds in TIMESTAMP values + * support for ActiveRecord 2.3.5 + * use ENV['TZ'] to set database session time zone + (as a result DATE and TIMESTAMP values are retrieved with correct time zone) + * added cache_columns adapter option + * added current_user adapter method + * added set_integer_columns and set_string_columns ActiveRecord model class methods +* Bug fixes: + * do not raise exception if ENV['PATH'] is nil + * do not add change_table behavior for ActiveRecord 2.0 (to avoid exception during loading) + * move foreign key definitions after definition of all tables in schema.rb + (to avoid definition of foreign keys before all tables are created) + * changed timestamp format mask to use ':' before fractional seconds + (workaround to avoid table detection in tables_in_string method in ActiveRecord associations.rb file) + * fixed custom create/update/delete methods with ActiveRecord 2.3+ and timestamps + * do not call oracle_enhanced specific schema dump methods when using other database adapters + +== 1.2.2 2009-09-28 + +* Enhancements + * improved RDoc documentation of public methods + * structure dump optionally (database.yml environment has db_stored_code: yes) extracts + packages, procedures, functions, views, triggers and synonyms + * automatically generated too long index names are shortened down to 30 characters + * create tables with primary key triggers + * use 'set_sequence_name :autogenerated' for inserting into legacy tables with trigger populated primary keys + * access to tables over database link (need to define local synonym to remote table and use local synonym in set_table_name) + * [JRuby] support JDBC connection using TNS_ADMIN environment variable and TNS database alias + * changed cursor_sharing option default from 'similar' to 'force' + * optional dbms_output logging to ActiveRecord log file (requires ruby-plsql gem) + * use add_foreign_key and remove_foreign_key to define foreign key constraints + (the same syntax as in http://github.com/matthuhiggins/foreigner and similar + to http://github.com/eyestreet/active_record_oracle_extensions) + * raise RecordNotUnique and InvalidForeignKey exceptions if caused by corresponding ORA errors + (these new exceptions are supported just by current ActiveRecord master branch) + * implemented disable_referential_integrity + (enables safe loading of fixtures in schema with foreign key constraints) + * use add_synonym and remove_synonym to define database synonyms + * add_foreign_key and add_synonym are also exported to schema.rb +* Bug fixes: + * [JRuby] do not raise LoadError if ojdbc14.jar cannot be required (rely on application server to add it to class path) + * [JRuby] 'execute' can be used to create triggers with :NEW reference + * support create_table without a block + * support create_table with Symbol table name + * use ActiveRecord functionality to do time zone conversion + * rake tasks such as db:test:clone are redefined only if oracle_enhanced is current adapter in use + * VARCHAR2 and CHAR column sizes are defined in characters and not in bytes (expected behavior from ActiveRecord) + * set_date_columns, set_datetime_columns, ignore_table_columns will work after reestablishing connection + * ignore :limit option for :text and :binary columns in migrations + * patches for ActiveRecord schema dumper to remove table prefixes and suffixes from schema.rb + +== 1.2.1 2009-06-07 + +* Enhancements + * caching of table indexes query which makes schema dump much faster +* Bug fixes: + * return Date (and not DateTime) values for :date column value before year 1970 + * fixed after_create/update/destroy callbacks with plsql custom methods + * fixed creation of large integers in JRuby + * Made test tasks respect RAILS_ENV + * fixed support for composite primary keys for tables with LOBs + +== 1.2.0 2009-03-22 + +* Enhancements + * support for JRuby and JDBC + * support for Ruby 1.9.1 and ruby-oci8 2.0 + * support for Rails 2.3 + * quoting of Oracle reserved words in table names and column names + * emulation of OracleAdapter (for ActiveRecord unit tests) +* Bug fixes: + * several bug fixes that were identified during running of ActiveRecord unit tests + +== 1.1.9 2009-01-02 + +* Enhancements + * Added support for table and column comments in migrations + * Added support for specifying sequence start values + * Added :privilege option (e.g. :SYSDBA) to ActiveRecord::Base.establish_connection +* Bug fixes: + * Do not mark empty decimals, strings and texts (stored as NULL in database) as changed when reassigning them (starting from Rails 2.1) + * Create booleans as VARCHAR2(1) columns if emulate_booleans_from_strings is true + +== 1.1.8 2008-10-10 + +* Bug fixes: + * Fixed storing of serialized LOB columns + * Prevent from SQL injection in :limit and :offset + * Order by LOB columns (by replacing column with function which returns first 100 characters of LOB) + * Sequence creation for tables with non-default primary key in create_table block + * Do count distinct workaround only when composite_primary_keys gem is used + (otherwise count distinct did not work with ActiveRecord 2.1.1) + * Fixed rake db:test:clone_structure task + (see http://rsim.lighthouseapp.com/projects/11468/tickets/11-rake-dbtestclone_structure-fails-in-117) + * Fixed bug when ActiveRecord::Base.allow_concurrency = true + (see http://dev.rubyonrails.org/ticket/11134) + +== 1.1.7 2008-08-20 + +* Bug fixes: + * Fixed that adapter works without ruby-plsql gem (in this case just custom create/update/delete methods are not available) + +== 1.1.6 2008-08-19 + +* Enhancements: + * Added support for set_date_columns and set_datetime_columns + * Added support for set_boolean_columns + * Added support for schema prefix in set_table_name (removed table name quoting) + * Added support for NVARCHAR2 column type +* Bug fixes: + * Do not call write_lobs callback when custom create or update methods are defined + +== 1.1.5 2008-07-27 + +* Bug fixes: + * Fixed that write_lobs callback works with partial_updates enabled (added additional record lock before writing BLOB data to database) +* Enhancements: + * Changed SQL SELECT in indexes method so that it will execute faster on some large data dictionaries + * Support for other date and time formats when assigning string to :date or :datetime column + +== 1.1.4 2008-07-14 + +* Enhancements: + * Date/Time quoting changes to support composite_primary_keys + * Added additional methods that are used by composite_primary_keys + +== 1.1.3 2008-07-10 + +* Enhancements: + * Added support for custom create, update and delete methods when working with legacy databases where + PL/SQL API should be used for create, update and delete operations + +== 1.1.2 2008-07-08 + +* Bug fixes: + * Fixed after_save callback addition for session store in ActiveRecord version 2.0.2 + * Changed date column name recognition - now should match regex /(^|_)date(_|$)/i + (previously "updated_at" was recognized as :date column and not as :datetime) + +== 1.1.1 2008-06-28 + +* Enhancements: + * Added ignore_table_columns option + * Added support for TIMESTAMP columns (without fractional seconds) + * NLS_DATE_FORMAT and NLS_TIMESTAMP_FORMAT independent DATE and TIMESTAMP columns support +* Bug fixes: + * Checks if CGI::Session::ActiveRecordStore::Session does not have enhanced_write_lobs callback before adding it + (Rails 2.0 does not add this callback, Rails 2.1 does) + +== 1.1.0 2008-05-05 + +* Forked from original activerecord-oracle-adapter-1.0.0.9216 +* Renamed oracle adapter to oracle_enhanced adapter + * Added "enhanced" to method and class definitions so that oracle_enhanced and original oracle adapter + could be used simultaniously + * Added Rails rake tasks as a copy from original oracle tasks +* Enhancements: + * Improved perfomance of schema dump methods when used on large data dictionaries + * Added LOB writing callback for sessions stored in database + * Added emulate_dates_by_column_name option + * Added emulate_integers_by_column_name option + * Added emulate_booleans_from_strings option diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-oracle_enhanced-adapter-1.4.0/License.txt b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-oracle_enhanced-adapter-1.4.0/License.txt new file mode 100644 index 00000000000..7b4cc05e90a --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-oracle_enhanced-adapter-1.4.0/License.txt @@ -0,0 +1,20 @@ +Copyright (c) 2008-2011 Graham Jenkins, Michael Schoen, Raimonds Simanovskis + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
\ No newline at end of file diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-oracle_enhanced-adapter-1.4.0/README.md b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-oracle_enhanced-adapter-1.4.0/README.md new file mode 100644 index 00000000000..794bf3b74eb --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-oracle_enhanced-adapter-1.4.0/README.md @@ -0,0 +1,378 @@ +activerecord-oracle_enhanced-adapter +==================================== + +Oracle enhanced adapter for ActiveRecord + +DESCRIPTION +----------- + +Oracle enhanced ActiveRecord adapter provides Oracle database access from Ruby on Rails applications. Oracle enhanced adapter can be used from Ruby on Rails versions 2.3.x and 3.x and it is working with Oracle database versions 10g and 11g. + +INSTALLATION +------------ + +### Rails 3 + +When using Ruby on Rails version 3 then in Gemfile include + + gem 'activerecord-oracle_enhanced-adapter', '~> 1.4.0' + +where instead of 1.4.0 you can specify any other desired version. It is recommended to specify version with `~>` which means that use specified version or later patch versions (in this example any later 1.4.x version but not 1.5.x version). Oracle enhanced adapter maintains API backwards compatibility during patch version upgrades and therefore it is safe to always upgrade to latest patch version. + +If you would like to use latest adapter version from github then specify + + gem 'activerecord-oracle_enhanced-adapter', :git => 'git://github.com/rsim/oracle-enhanced.git' + +If you are using MRI 1.8 or 1.9 Ruby implementation then you need to install ruby-oci8 gem as well as Oracle client, e.g. [Oracle Instant Client](http://www.oracle.com/technetwork/database/features/instant-client/index-097480.html). Include in Gemfile also ruby-oci8: + + gem 'ruby-oci8', '~> 2.0.6' + +If you are using JRuby then you need to download latest [Oracle JDBC driver](http://www.oracle.com/technetwork/database/enterprise-edition/jdbc-112010-090769.html) - either ojdbc6.jar for Java 6 or ojdbc5.jar for Java 5. And copy this file to one of these locations: + + * in `./lib` directory of Rails application + * in some directory which is in `PATH` + * in `JRUBY_HOME/lib` directory + * or include path to JDBC driver jar file in Java `CLASSPATH` + +After specifying necessary gems in Gemfile run + + bundle install + +to install the adapter (or later run `bundle update` to force updating to latest version). + +### Rails 2.3 + +If you don't use Bundler in Rails 2 application then you need to specify gems in `config/environment.rb`, e.g. + + Rails::Initializer.run do |config| + #... + config.gem 'activerecord-oracle_enhanced-adapter', :lib => "active_record/connection_adapters/oracle_enhanced_adapter" + config.gem 'ruby-oci8' + #... + end + +But it is recommended to use Bundler for gem version management also for Rails 2.3 applications (search for instructions in Google). + +### Without Rails and Bundler + +If you want to use ActiveRecord and Oracle enhanced adapter without Rails and Bundler then install it just as a gem: + + gem install activerecord-oracle_enhanced-adapter + +USAGE +----- + +### Database connection + +In Rails application `config/database.yml` use oracle_enhanced as adapter name, e.g. + + development: + adapter: oracle_enhanced + database: xe + username: user + password: secret + +If `TNS_ADMIN` environment variable is pointing to directory where `tnsnames.ora` file is located then you can use TNS connection name in `database` parameter. Otherwise you can directly specify database host, port (defaults to 1521) and database name in the following way: + + development: + adapter: oracle_enhanced + host: localhost + port: 1521 + database: xe + username: user + password: secret + +or you can use Oracle specific format in `database` parameter: + + development: + adapter: oracle_enhanced + database: //localhost:1521/xe + username: user + password: secret + +or you can even use Oracle specific TNS connection description: + + development: + adapter: oracle_enhanced + database: "(DESCRIPTION=(ADDRESS_LIST=(ADDRESS=(PROTOCOL=tcp)(HOST=localhost)(PORT=1521)))(CONNECT_DATA=(SERVICE_NAME=xe)))" + username: user + password: secret + +If you deploy JRuby on Rails application in Java application server that supports JNDI connections then you can specify JNDI connection as well: + + development: + adapter: oracle_enhanced + jndi: "jdbc/jndi_connection_name" + +You can find other available database.yml connection parameters in [oracle_enhanced_adapter.rb](/rsim/oracle-enhanced/blob/master/lib/active_record/connection_adapters/oracle_enhanced_adapter.rb). There are many NLS settings as well as some other Oracle session settings. + +### Adapter settings + +If you want to change Oracle enhanced adapter default settings then create initializer file e.g. `config/initializers/oracle.rb` specify there necessary defaults, e.g.: + + # It is recommended to set time zone in TZ environment variable so that the same timezone will be used by Ruby and by Oracle session + ENV['TZ'] = 'UTC' + + ActiveSupport.on_load(:active_record) do + ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.class_eval do + # id columns and columns which end with _id will always be converted to integers + self.emulate_integers_by_column_name = true + # DATE columns which include "date" in name will be converted to Date, otherwise to Time + self.emulate_dates_by_column_name = true + # true and false will be stored as 'Y' and 'N' + self.emulate_booleans_from_strings = true + # start primary key sequences from 1 (and not 10000) and take just one next value in each session + self.default_sequence_start_value = "1 NOCACHE INCREMENT BY 1" + # other settings ... + end + end + +In case of Rails 2 application you do not need to use `ActiveSupport.on_load(:active_record) do ... end` around settings code block. + +See other adapter settings in [oracle_enhanced_adapter.rb](/rsim/oracle-enhanced/blob/master/lib/active_record/connection_adapters/oracle_enhanced_adapter.rb). + +### Legacy schema support + +If you want to put Oracle enhanced adapter on top of existing schema tables then there are several methods how to override ActiveRecord defaults, see example: + + class Employee < ActiveRecord::Base + # specify schema and table name + set_table_name "hr.hr_employees" + # specify primary key name + set_primary_key "employee_id" + # specify sequence name + set_sequence_name "hr.hr_employee_s" + # set which DATE columns should be converted to Ruby Date + set_date_columns :hired_on, :birth_date_on + # set which DATE columns should be converted to Ruby Time + set_datetime_columns :last_login_time + # set which VARCHAR2 columns should be converted to true and false + set_boolean_columns :manager, :active + # set which columns should be ignored in ActiveRecord + ignore_table_columns :attribute1, :attribute2 + end + +You can also access remote tables over database link using + + set_table_name "hr_employees@db_link" + +### Custom create, update and delete methods + +If you have legacy schema and you are not allowed to do direct INSERTs, UPDATEs and DELETEs in legacy schema tables and need to use existing PL/SQL procedures for create, updated, delete operations then you should add `ruby-plsql` gem to your application and then define custom create, update and delete methods, see example: + + class Employee < ActiveRecord::Base + # when defining create method then return ID of new record that will be assigned to id attribute of new object + set_create_method do + plsql.employees_pkg.create_employee( + :p_first_name => first_name, + :p_last_name => last_name, + :p_employee_id => nil + )[:p_employee_id] + end + set_update_method do + plsql.employees_pkg.update_employee( + :p_employee_id => id, + :p_first_name => first_name, + :p_last_name => last_name + ) + end + set_delete_method do + plsql.employees_pkg.delete_employee( + :p_employee_id => id + ) + end + end + +In addition in `config/initializers/oracle.rb` initializer specify that ruby-plsql should use ActiveRecord database connection: + + plsql.activerecord_class = ActiveRecord::Base + +### Oracle CONTEXT index support + +Every edition of Oracle database includes [Oracle Text](http://www.oracle.com/technology/products/text/index.html) option for free which provides several full text indexing capabilities. Therefore in Oracle database case you don’t need external full text indexing and searching engines which can simplify your application deployment architecture. + +To create simple single column index create migration with, e.g. + + add_context_index :posts, :title + +and you can remove context index with + + remove_context_index :posts, :title + +Include in class definition + + has_context_index + +and then you can do full text search with + + Post.contains(:title, 'word') + +You can create index on several columns (which will generate additional stored procedure for providing XML document with specified columns to indexer): + + add_context_index :posts, [:title, :body] + +And you can search either in all columns or specify in which column you want to search (as first argument you need to specify first column name as this is the column which is referenced during index creation): + + Post.contains(:title, 'word') + Post.contains(:title, 'word within title') + Post.contains(:title, 'word within body') + +See Oracle Text documentation for syntax that you can use in CONTAINS function in SELECT WHERE clause. + +You can also specify some dummy main column name when creating multiple column index as well as specify to update index automatically after each commit (as otherwise you need to synchronize index manually or schedule periodic update): + + add_context_index :posts, [:title, :body], :index_column => :all_text, :sync => 'ON COMMIT' + + Post.contains(:all_text, 'word') + +Or you can specify that index should be updated when specified columns are updated (e.g. in ActiveRecord you can specify to trigger index update when created_at or updated_at columns are updated). Otherwise index is updated only when main index column is updated. + + add_context_index :posts, [:title, :body], :index_column => :all_text, + :sync => 'ON COMMIT', :index_column_trigger_on => [:created_at, :updated_at] + +And you can even create index on multiple tables by providing SELECT statements which should be used to fetch necessary columns from related tables: + + add_context_index :posts, + [:title, :body, + # specify aliases always with AS keyword + "SELECT comments.author AS comment_author, comments.body AS comment_body FROM comments WHERE comments.post_id = :id" + ], + :name => 'post_and_comments_index', + :index_column => :all_text, + :index_column_trigger_on => [:updated_at, :comments_count], + :sync => 'ON COMMIT' + + # search in any table columns + Post.contains(:all_text, 'word') + # search in specified column + Post.contains(:all_text, "aaa within title") + Post.contains(:all_text, "bbb within comment_author") + +### Oracle specific schema statements and data types + +There are several additional schema statements and data types available that you can use in database migrations: + + * `add_foreign_key` and `remove_foreign_key` for foreign key definition (and they are also dumped in `db/schema.rb`) + * `add_synonym` and `remove_synonym` for synonym definition (and they are also dumped in `db/schema.rb`) + * You can create table with primary key trigger using `:primary_key_trigger => true` option for `create_table` + * You can define columns with `raw` type which maps to Oracle's `RAW` type + * You can add table and column comments with `:comment` option + * On Oracle 11g you can define `virtual` columns with calculation formula in `:default` option + * Default tablespaces can be specified for tables, indexes, clobs and blobs, for example: + + ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.default_tablespaces = + {:clob => 'TS_LOB', :blob => 'TS_LOB', :index => 'TS_INDEX', :table => 'TS_DATA'} + +TROUBLESHOOTING +--------------- + +### What to do if Oracle enhanced adapter is not working? + +Please verify that + + 1. Oracle Instant Client is installed correctly + Can you connect to database using sqlnet? + + 2. ruby-oci8 is installed correctly + Try something like: + + ruby -rubygems -e "require 'oci8'; OCI8.new('username','password','database').exec('select * from dual') do |r| puts r.join(','); end" + + to verify that ruby-oci8 is working + + 3. Verify that activerecord-oracle_enhanced-adapter is working from irb + + require 'rubygems' + gem 'activerecord' + gem 'activerecord-oracle_enhanced-adapter' + require 'activerecord' + ActiveRecord::Base.establish_connection(:adapter => "oracle_enhanced", :database => "database",:username => "user",:password => "password") + + and see if it is successful (use your correct database, username and password) + +### What to do if Oracle enhanced adapter is not working with Phusion Passenger? + +Oracle Instant Client and ruby-oci8 requires that several environment variables are set: + + * `LD_LIBRARY_PATH` (on Linux) or `DYLD_LIBRARY_PATH` (on Mac) should point to Oracle Instant Client directory (where Oracle client shared libraries are located) + * `TNS_ADMIN` should point to directory where `tnsnames.ora` file is located + * `NLS_LANG` should specify which territory and language NLS settings to use and which character set to use (e.g. `"AMERICAN_AMERICA.UTF8"`) + +If this continues to throw "OCI Library Initialization Error (OCIError)", you might also need + + * `ORACLE_HOME` set to full Oracle client installation directory + +When Apache with Phusion Passenger (mod_passenger or previously mod_rails) is used for Rails application deployment then by default Ruby is launched without environment variables that you have set in shell profile scripts (e.g. .profile). Therefore it is necessary to set environment variables in one of the following ways: + + * Create wrapper script as described in [Phusion blog](http://blog.phusion.nl/2008/12/16/passing-environment-variables-to-ruby-from-phusion-passenger) or [RayApps::Blog](http://blog.rayapps.com/2008/05/21/using-mod_rails-with-rails-applications-on-oracle) + * Set environment variables in the file which is used by Apache before launching Apache worker processes - on Linux it typically is envvars file (look in apachectl or apache2ctl script where it is looking for envvars file) or /System/Library/LaunchDaemons/org.apache.httpd.plist on Mac OS X. See the following [discussion thread](http://groups.google.com/group/oracle-enhanced/browse_thread/thread/c5f64106569fadd0) for more hints. + +RUNNING TESTS +------------- + +See [RUNNING_TESTS.md](/rsim/oracle-enhanced/blob/master/RUNNING_TESTS.md) for information how to set up environment and run Oracle enhanced adapter unit tests. + +LINKS +----- + +* Source code: http://github.com/rsim/oracle-enhanced +* Bug reports / Feature requests / Pull requests: http://github.com/rsim/oracle-enhanced/issues +* Discuss at Oracle enhanced adapter group: http://groups.google.com/group/oracle-enhanced +* Blog posts about Oracle enhanced adapter can be found at http://blog.rayapps.com/category/oracle_enhanced + +CONTRIBUTORS +------------ + +* Raimonds Simanovskis +* Jorge Dias +* James Wylder +* Rob Christie +* Nate Wieger +* Edgars Beigarts +* Lachlan Laycock +* toddwf +* Anton Jenkins +* Dave Smylie +* Alex Rothenberg +* Billy Reisinger +* David Blain +* Joe Khoobyar +* Edvard Majakari +* Beau Fabry +* Simon Chiang +* Peter Nyberg +* Dwayne Litzenberger +* Aaron Patterson +* Darcy Schultz +* Alexi Rahman +* Joeri Samson +* Luca Bernardo Ciddio +* Sam Baskinger +* Benjamin Ortega +* Yasuo Honda + +LICENSE +------- + +(The MIT License) + +Copyright (c) 2008-2011 Graham Jenkins, Michael Schoen, Raimonds Simanovskis + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
\ No newline at end of file diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-oracle_enhanced-adapter-1.4.0/RUNNING_TESTS.md b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-oracle_enhanced-adapter-1.4.0/RUNNING_TESTS.md new file mode 100644 index 00000000000..87655f85247 --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-oracle_enhanced-adapter-1.4.0/RUNNING_TESTS.md @@ -0,0 +1,45 @@ +Creating the test database +-------------------------- + +You need Oracle database (version 10.2 or later) with SYS and SYSTEM user access. + +If you are on a Mac OS X 10.6 then use [these instructions](http://blog.rayapps.com/2009/09/14/how-to-install-oracle-database-10g-on-mac-os-x-snow-leopard) to install local Oracle DB 10.2.0.4. Other option is to use Linux VM and install Oracle DB on it. + +If you are on Linux (or will use Linux virtual machine) and need Oracle DB just for running tests then Oracle DB XE edition is enough. See [Oracle XE downloads page](http://www.oracle.com/technetwork/database/express-edition/downloads/index.html) for download links and instructions. + +If you are getting ORA-12520 errors when running tests then it means that Oracle cannot create enough processes to handle many connections (as during tests many connections are created and destroyed). In this case you need to log in as SYSTEM user and execute e.g. + + alter system set processes=200 scope=spfile; + +to increase process limit and then restart the database (this will be necessary if Oracle XE will be used as default processes limit is 40). + +Ruby versions +------------- + +It is recommended to use [RVM](http://rvm.beginrescueend.com) to run tests with different Ruby implementations. oracle_enhanced is mainly tested with MRI 1.8.7 (all Rails versions) and 1.9.2 (Rails 3) and JRuby 1.6. + +Running tests +------------- + +* Create Oracle database schema for test purposes. Review `spec/spec_helper.rb` to see default schema/user names and database names (use environment variables to override defaults) + + SQL> CREATE USER oracle_enhanced IDENTIFIED BY oracle_enhanced; + SQL> GRANT unlimited tablespace, create session, create table, create sequence, create procedure, create trigger, create view, create materialized view, create database link, create synonym, create type, ctxapp TO oracle_enhanced; + +* If you use RVM then switch to corresponding Ruby (1.8.7, 1.9.2 or JRuby) and it is recommended to create isolated gemset for test purposes (e.g. rvm create gemset oracle_enhanced) + +* Install bundler with + + gem install bundler + +* Set RAILS_GEM_VERSION to Rails version that you would like to use in oracle_enhanced tests, e.g. + + export RAILS_GEM_VERSION=3.0.3 + +* Install necessary gems with + + bundle install + +* Run tests with + + rake spec diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-oracle_enhanced-adapter-1.4.0/Rakefile b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-oracle_enhanced-adapter-1.4.0/Rakefile new file mode 100644 index 00000000000..e757684e7d8 --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-oracle_enhanced-adapter-1.4.0/Rakefile @@ -0,0 +1,46 @@ +require 'rubygems' +require 'bundler' +begin + Bundler.setup(:default, :development) +rescue Bundler::BundlerError => e + $stderr.puts e.message + $stderr.puts "Run `bundle install` to install missing gems" + exit e.status_code +end + +require 'rake' + +require 'jeweler' +Jeweler::Tasks.new do |gem| + gem.name = "activerecord-oracle_enhanced-adapter" + gem.summary = "Oracle enhanced adapter for ActiveRecord" + gem.description = <<-EOS +Oracle "enhanced" ActiveRecord adapter contains useful additional methods for working with new and legacy Oracle databases. +This adapter is superset of original ActiveRecord Oracle adapter. +EOS + gem.email = "raimonds.simanovskis@gmail.com" + gem.homepage = "http://github.com/rsim/oracle-enhanced" + gem.authors = ["Raimonds Simanovskis"] + gem.extra_rdoc_files = ['README.md'] +end +Jeweler::RubygemsDotOrgTasks.new + +require 'rspec/core/rake_task' +RSpec::Core::RakeTask.new(:spec) + +RSpec::Core::RakeTask.new(:rcov) do |t| + t.rcov = true + t.rcov_opts = ['--exclude', '/Library,spec/'] +end + +task :default => :spec + +require 'rake/rdoctask' +Rake::RDocTask.new do |rdoc| + version = File.exist?('VERSION') ? File.read('VERSION') : "" + + rdoc.rdoc_dir = 'doc' + rdoc.title = "activerecord-oracle_enhanced-adapter #{version}" + rdoc.rdoc_files.include('README*') + rdoc.rdoc_files.include('lib/**/*.rb') +end diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-oracle_enhanced-adapter-1.4.0/VERSION b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-oracle_enhanced-adapter-1.4.0/VERSION new file mode 100644 index 00000000000..88c5fb891dc --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-oracle_enhanced-adapter-1.4.0/VERSION @@ -0,0 +1 @@ +1.4.0 diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-oracle_enhanced-adapter-1.4.0/activerecord-oracle_enhanced-adapter.gemspec b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-oracle_enhanced-adapter-1.4.0/activerecord-oracle_enhanced-adapter.gemspec new file mode 100644 index 00000000000..9833bab3f9c --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-oracle_enhanced-adapter-1.4.0/activerecord-oracle_enhanced-adapter.gemspec @@ -0,0 +1,127 @@ +# Generated by jeweler +# DO NOT EDIT THIS FILE DIRECTLY +# Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec' +# -*- encoding: utf-8 -*- + +Gem::Specification.new do |s| + s.name = %q{activerecord-oracle_enhanced-adapter} + s.version = "1.4.0" + + s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= + s.authors = ["Raimonds Simanovskis"] + s.date = %q{2011-08-09} + s.description = %q{Oracle "enhanced" ActiveRecord adapter contains useful additional methods for working with new and legacy Oracle databases. +This adapter is superset of original ActiveRecord Oracle adapter. +} + s.email = %q{raimonds.simanovskis@gmail.com} + s.extra_rdoc_files = [ + "README.md" + ] + s.files = [ + ".rspec", + "Gemfile", + "History.txt", + "License.txt", + "README.md", + "RUNNING_TESTS.md", + "Rakefile", + "VERSION", + "activerecord-oracle_enhanced-adapter.gemspec", + "lib/active_record/connection_adapters/emulation/oracle_adapter.rb", + "lib/active_record/connection_adapters/oracle_enhanced.rake", + "lib/active_record/connection_adapters/oracle_enhanced_activerecord_patches.rb", + "lib/active_record/connection_adapters/oracle_enhanced_adapter.rb", + "lib/active_record/connection_adapters/oracle_enhanced_base_ext.rb", + "lib/active_record/connection_adapters/oracle_enhanced_column.rb", + "lib/active_record/connection_adapters/oracle_enhanced_connection.rb", + "lib/active_record/connection_adapters/oracle_enhanced_context_index.rb", + "lib/active_record/connection_adapters/oracle_enhanced_core_ext.rb", + "lib/active_record/connection_adapters/oracle_enhanced_cpk.rb", + "lib/active_record/connection_adapters/oracle_enhanced_dirty.rb", + "lib/active_record/connection_adapters/oracle_enhanced_jdbc_connection.rb", + "lib/active_record/connection_adapters/oracle_enhanced_oci_connection.rb", + "lib/active_record/connection_adapters/oracle_enhanced_procedures.rb", + "lib/active_record/connection_adapters/oracle_enhanced_schema_definitions.rb", + "lib/active_record/connection_adapters/oracle_enhanced_schema_dumper.rb", + "lib/active_record/connection_adapters/oracle_enhanced_schema_statements.rb", + "lib/active_record/connection_adapters/oracle_enhanced_schema_statements_ext.rb", + "lib/active_record/connection_adapters/oracle_enhanced_structure_dump.rb", + "lib/active_record/connection_adapters/oracle_enhanced_tasks.rb", + "lib/active_record/connection_adapters/oracle_enhanced_version.rb", + "lib/activerecord-oracle_enhanced-adapter.rb", + "spec/active_record/connection_adapters/oracle_enhanced_adapter_spec.rb", + "spec/active_record/connection_adapters/oracle_enhanced_connection_spec.rb", + "spec/active_record/connection_adapters/oracle_enhanced_context_index_spec.rb", + "spec/active_record/connection_adapters/oracle_enhanced_core_ext_spec.rb", + "spec/active_record/connection_adapters/oracle_enhanced_cpk_spec.rb", + "spec/active_record/connection_adapters/oracle_enhanced_data_types_spec.rb", + "spec/active_record/connection_adapters/oracle_enhanced_dbms_output_spec.rb", + "spec/active_record/connection_adapters/oracle_enhanced_dirty_spec.rb", + "spec/active_record/connection_adapters/oracle_enhanced_emulate_oracle_adapter_spec.rb", + "spec/active_record/connection_adapters/oracle_enhanced_procedures_spec.rb", + "spec/active_record/connection_adapters/oracle_enhanced_schema_dump_spec.rb", + "spec/active_record/connection_adapters/oracle_enhanced_schema_statements_spec.rb", + "spec/active_record/connection_adapters/oracle_enhanced_structure_dump_spec.rb", + "spec/spec_helper.rb" + ] + s.homepage = %q{http://github.com/rsim/oracle-enhanced} + s.require_paths = ["lib"] + s.rubygems_version = %q{1.6.2} + s.summary = %q{Oracle enhanced adapter for ActiveRecord} + s.test_files = [ + "spec/active_record/connection_adapters/oracle_enhanced_adapter_spec.rb", + "spec/active_record/connection_adapters/oracle_enhanced_connection_spec.rb", + "spec/active_record/connection_adapters/oracle_enhanced_context_index_spec.rb", + "spec/active_record/connection_adapters/oracle_enhanced_core_ext_spec.rb", + "spec/active_record/connection_adapters/oracle_enhanced_cpk_spec.rb", + "spec/active_record/connection_adapters/oracle_enhanced_data_types_spec.rb", + "spec/active_record/connection_adapters/oracle_enhanced_dbms_output_spec.rb", + "spec/active_record/connection_adapters/oracle_enhanced_dirty_spec.rb", + "spec/active_record/connection_adapters/oracle_enhanced_emulate_oracle_adapter_spec.rb", + "spec/active_record/connection_adapters/oracle_enhanced_procedures_spec.rb", + "spec/active_record/connection_adapters/oracle_enhanced_schema_dump_spec.rb", + "spec/active_record/connection_adapters/oracle_enhanced_schema_statements_spec.rb", + "spec/active_record/connection_adapters/oracle_enhanced_structure_dump_spec.rb", + "spec/spec_helper.rb" + ] + + if s.respond_to? :specification_version then + s.specification_version = 3 + + if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then + s.add_development_dependency(%q<jeweler>, ["~> 1.5.1"]) + s.add_development_dependency(%q<rspec>, ["~> 2.4"]) + s.add_development_dependency(%q<activerecord>, [">= 0"]) + s.add_development_dependency(%q<activemodel>, [">= 0"]) + s.add_development_dependency(%q<activesupport>, [">= 0"]) + s.add_development_dependency(%q<actionpack>, [">= 0"]) + s.add_development_dependency(%q<railties>, [">= 0"]) + s.add_development_dependency(%q<arel>, [">= 0"]) + s.add_development_dependency(%q<ruby-plsql>, [">= 0.4.4"]) + s.add_development_dependency(%q<ruby-oci8>, ["~> 2.0.4"]) + else + s.add_dependency(%q<jeweler>, ["~> 1.5.1"]) + s.add_dependency(%q<rspec>, ["~> 2.4"]) + s.add_dependency(%q<activerecord>, [">= 0"]) + s.add_dependency(%q<activemodel>, [">= 0"]) + s.add_dependency(%q<activesupport>, [">= 0"]) + s.add_dependency(%q<actionpack>, [">= 0"]) + s.add_dependency(%q<railties>, [">= 0"]) + s.add_dependency(%q<arel>, [">= 0"]) + s.add_dependency(%q<ruby-plsql>, [">= 0.4.4"]) + s.add_dependency(%q<ruby-oci8>, ["~> 2.0.4"]) + end + else + s.add_dependency(%q<jeweler>, ["~> 1.5.1"]) + s.add_dependency(%q<rspec>, ["~> 2.4"]) + s.add_dependency(%q<activerecord>, [">= 0"]) + s.add_dependency(%q<activemodel>, [">= 0"]) + s.add_dependency(%q<activesupport>, [">= 0"]) + s.add_dependency(%q<actionpack>, [">= 0"]) + s.add_dependency(%q<railties>, [">= 0"]) + s.add_dependency(%q<arel>, [">= 0"]) + s.add_dependency(%q<ruby-plsql>, [">= 0.4.4"]) + s.add_dependency(%q<ruby-oci8>, ["~> 2.0.4"]) + end +end + diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-oracle_enhanced-adapter-1.4.0/lib/active_record/connection_adapters/emulation/oracle_adapter.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-oracle_enhanced-adapter-1.4.0/lib/active_record/connection_adapters/emulation/oracle_adapter.rb new file mode 100644 index 00000000000..94254dc8105 --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-oracle_enhanced-adapter-1.4.0/lib/active_record/connection_adapters/emulation/oracle_adapter.rb @@ -0,0 +1,5 @@ +class ActiveRecord::ConnectionAdapters::OracleAdapter < ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter #:nodoc: + def adapter_name + 'Oracle' + end +end diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-oracle_enhanced-adapter-1.4.0/lib/active_record/connection_adapters/oracle_enhanced.rake b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-oracle_enhanced-adapter-1.4.0/lib/active_record/connection_adapters/oracle_enhanced.rake new file mode 100644 index 00000000000..38802a16b2c --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-oracle_enhanced-adapter-1.4.0/lib/active_record/connection_adapters/oracle_enhanced.rake @@ -0,0 +1,96 @@ +# implementation idea taken from JDBC adapter +# added possibility to execute previously defined task (passed as argument to task block) +def redefine_task(*args, &block) + task_name = Hash === args.first ? args.first.keys[0] : args.first + existing_task = Rake.application.lookup task_name + existing_actions = nil + if existing_task + class << existing_task; public :instance_variable_set, :instance_variable_get; end + existing_task.instance_variable_set "@prerequisites", FileList[] + existing_actions = existing_task.instance_variable_get "@actions" + existing_task.instance_variable_set "@actions", [] + end + task(*args) do + block.call(existing_actions) + end +end + +# Creates database user with db:create +def create_database_with_oracle_enhanced(config) + if config['adapter'] == 'oracle_enhanced' + print "Please provide the SYSTEM password for your oracle installation\n>" + system_password = $stdin.gets.strip + ActiveRecord::Base.establish_connection(config.merge('username' => 'SYSTEM', 'password' => system_password)) + ActiveRecord::Base.connection.execute "DROP USER #{config['username']} CASCADE" rescue nil + ActiveRecord::Base.connection.execute "CREATE USER #{config['username']} IDENTIFIED BY #{config['password']}" + ActiveRecord::Base.connection.execute "GRANT unlimited tablespace TO #{config['username']}" + ActiveRecord::Base.connection.execute "GRANT create session TO #{config['username']}" + ActiveRecord::Base.connection.execute "GRANT create table TO #{config['username']}" + ActiveRecord::Base.connection.execute "GRANT create sequence TO #{config['username']}" + else + create_database_without_oracle_enhanced(config) + end +end +alias :create_database_without_oracle_enhanced :create_database +alias :create_database :create_database_with_oracle_enhanced + +# Drops database user with db:drop +def drop_database_with_oracle_enhanced(config) + if config['adapter'] == 'oracle_enhanced' + print "Please provide the SYSTEM password for your oracle installation\n>" + system_password = $stdin.gets.strip + ActiveRecord::Base.establish_connection(config.merge('username' => 'SYSTEM', 'password' => system_password)) + ActiveRecord::Base.connection.execute "DROP USER #{config['username']} CASCADE" + else + drop_database_without_oracle_enhanced(config) + end +end +alias :drop_database_without_oracle_enhanced :drop_database +alias :drop_database :drop_database_with_oracle_enhanced + +namespace :db do + + namespace :structure do + redefine_task :dump => :environment do |existing_actions| + abcs = ActiveRecord::Base.configurations + rails_env = defined?(Rails.env) ? Rails.env : RAILS_ENV + if abcs[rails_env]['adapter'] == 'oracle_enhanced' + ActiveRecord::Base.establish_connection(abcs[rails_env]) + File.open("db/#{rails_env}_structure.sql", "w+") { |f| f << ActiveRecord::Base.connection.structure_dump } + if ActiveRecord::Base.connection.supports_migrations? + File.open("db/#{rails_env}_structure.sql", "a") { |f| f << ActiveRecord::Base.connection.dump_schema_information } + end + if abcs[rails_env]['structure_dump'] == "db_stored_code" + File.open("db/#{rails_env}_structure.sql", "a") { |f| f << ActiveRecord::Base.connection.structure_dump_db_stored_code } + end + else + Array(existing_actions).each{|action| action.call} + end + end + end + + namespace :test do + redefine_task :clone_structure => [ "db:structure:dump", "db:test:purge" ] do |existing_actions| + abcs = ActiveRecord::Base.configurations + rails_env = defined?(Rails.env) ? Rails.env : RAILS_ENV + if abcs[rails_env]['adapter'] == 'oracle_enhanced' && abcs['test']['adapter'] == 'oracle_enhanced' + ActiveRecord::Base.establish_connection(:test) + ActiveRecord::Base.connection.execute_structure_dump(File.read("db/#{rails_env}_structure.sql")) + else + Array(existing_actions).each{|action| action.call} + end + end + + redefine_task :purge => :environment do |existing_actions| + abcs = ActiveRecord::Base.configurations + if abcs['test']['adapter'] == 'oracle_enhanced' + ActiveRecord::Base.establish_connection(:test) + ActiveRecord::Base.connection.execute_structure_dump(ActiveRecord::Base.connection.full_drop) + ActiveRecord::Base.connection.execute("PURGE RECYCLEBIN") rescue nil + else + Array(existing_actions).each{|action| action.call} + end + end + + end +end diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-oracle_enhanced-adapter-1.4.0/lib/active_record/connection_adapters/oracle_enhanced_activerecord_patches.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-oracle_enhanced-adapter-1.4.0/lib/active_record/connection_adapters/oracle_enhanced_activerecord_patches.rb new file mode 100644 index 00000000000..ddce35c181f --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-oracle_enhanced-adapter-1.4.0/lib/active_record/connection_adapters/oracle_enhanced_activerecord_patches.rb @@ -0,0 +1,41 @@ +# ActiveRecord 2.3 patches +if ActiveRecord::VERSION::MAJOR == 2 && ActiveRecord::VERSION::MINOR == 3 + require "active_record/associations" + + ActiveRecord::Associations::ClassMethods.module_eval do + private + def tables_in_string(string) + return [] if string.blank? + if self.connection.adapter_name == "OracleEnhanced" + # always convert table names to downcase as in Oracle quoted table names are in uppercase + # ignore raw_sql_ that is used by Oracle adapter as alias for limit/offset subqueries + string.scan(/([a-zA-Z_][\.\w]+).?\./).flatten.map(&:downcase).uniq - ['raw_sql_'] + else + string.scan(/([\.a-zA-Z_]+).?\./).flatten + end + end + end + + ActiveRecord::Associations::ClassMethods::JoinDependency::JoinAssociation.class_eval do + protected + def aliased_table_name_for(name, suffix = nil) + # always downcase quoted table name as Oracle quoted table names are in uppercase + if !parent.table_joins.blank? && parent.table_joins.to_s.downcase =~ %r{join(\s+\w+)?\s+#{active_record.connection.quote_table_name(name).downcase}\son} + @join_dependency.table_aliases[name] += 1 + end + + unless @join_dependency.table_aliases[name].zero? + # if the table name has been used, then use an alias + name = active_record.connection.table_alias_for "#{pluralize(reflection.name)}_#{parent_table_name}#{suffix}" + table_index = @join_dependency.table_aliases[name] + @join_dependency.table_aliases[name] += 1 + name = name[0..active_record.connection.table_alias_length-3] + "_#{table_index+1}" if table_index > 0 + else + @join_dependency.table_aliases[name] += 1 + end + + name + end + end + +end diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-oracle_enhanced-adapter-1.4.0/lib/active_record/connection_adapters/oracle_enhanced_adapter.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-oracle_enhanced-adapter-1.4.0/lib/active_record/connection_adapters/oracle_enhanced_adapter.rb new file mode 100644 index 00000000000..4be974af279 --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-oracle_enhanced-adapter-1.4.0/lib/active_record/connection_adapters/oracle_enhanced_adapter.rb @@ -0,0 +1,1321 @@ +# -*- coding: utf-8 -*- +# oracle_enhanced_adapter.rb -- ActiveRecord adapter for Oracle 8i, 9i, 10g, 11g +# +# Authors or original oracle_adapter: Graham Jenkins, Michael Schoen +# +# Current maintainer: Raimonds Simanovskis (http://blog.rayapps.com) +# +######################################################################### +# +# See History.txt for changes added to original oracle_adapter.rb +# +######################################################################### +# +# From original oracle_adapter.rb: +# +# Implementation notes: +# 1. Redefines (safely) a method in ActiveRecord to make it possible to +# implement an autonumbering solution for Oracle. +# 2. The OCI8 driver is patched to properly handle values for LONG and +# TIMESTAMP columns. The driver-author has indicated that a future +# release of the driver will obviate this patch. +# 3. LOB support is implemented through an after_save callback. +# 4. Oracle does not offer native LIMIT and OFFSET options; this +# functionality is mimiced through the use of nested selects. +# See http://asktom.oracle.com/pls/ask/f?p=4950:8:::::F4950_P8_DISPLAYID:127412348064 +# +# Do what you want with this code, at your own peril, but if any +# significant portion of my code remains then please acknowledge my +# contribution. +# portions Copyright 2005 Graham Jenkins + +# ActiveRecord 2.2 does not load version file automatically +require 'active_record/version' unless defined?(ActiveRecord::VERSION) + +require 'active_record/connection_adapters/abstract_adapter' +require 'active_record/connection_adapters/oracle_enhanced_connection' + +require 'active_record/connection_adapters/oracle_enhanced_base_ext' +require 'active_record/connection_adapters/oracle_enhanced_column' + +require 'digest/sha1' + +module ActiveRecord + module ConnectionAdapters #:nodoc: + + # Oracle enhanced adapter will work with both + # Ruby 1.8/1.9 ruby-oci8 gem (which provides interface to Oracle OCI client) + # or with JRuby and Oracle JDBC driver. + # + # It should work with Oracle 9i, 10g and 11g databases. + # Limited set of functionality should work on Oracle 8i as well but several features + # rely on newer functionality in Oracle database. + # + # Usage notes: + # * Key generation assumes a "${table_name}_seq" sequence is available + # for all tables; the sequence name can be changed using + # ActiveRecord::Base.set_sequence_name. When using Migrations, these + # sequences are created automatically. + # Use set_sequence_name :autogenerated with legacy tables that have + # triggers that populate primary keys automatically. + # * Oracle uses DATE or TIMESTAMP datatypes for both dates and times. + # Consequently some hacks are employed to map data back to Date or Time + # in Ruby. Timezones and sub-second precision on timestamps are + # not supported. + # * Default values that are functions (such as "SYSDATE") are not + # supported. This is a restriction of the way ActiveRecord supports + # default values. + # + # Required parameters: + # + # * <tt>:username</tt> + # * <tt>:password</tt> + # * <tt>:database</tt> - either TNS alias or connection string for OCI client or database name in JDBC connection string + # + # Optional parameters: + # + # * <tt>:host</tt> - host name for JDBC connection, defaults to "localhost" + # * <tt>:port</tt> - port number for JDBC connection, defaults to 1521 + # * <tt>:privilege</tt> - set "SYSDBA" if you want to connect with this privilege + # * <tt>:allow_concurrency</tt> - set to "true" if non-blocking mode should be enabled (just for OCI client) + # * <tt>:prefetch_rows</tt> - how many rows should be fetched at one time to increase performance, defaults to 100 + # * <tt>:cursor_sharing</tt> - cursor sharing mode to minimize amount of unique statements, defaults to "force" + # * <tt>:time_zone</tt> - database session time zone + # (it is recommended to set it using ENV['TZ'] which will be then also used for database session time zone) + # + # Optionals NLS parameters: + # + # * <tt>:nls_calendar</tt> + # * <tt>:nls_characterset</tt> + # * <tt>:nls_comp</tt> + # * <tt>:nls_currency</tt> + # * <tt>:nls_date_format</tt> - format for :date columns, defaults to <tt>YYYY-MM-DD HH24:MI:SS</tt> + # * <tt>:nls_date_language</tt> + # * <tt>:nls_dual_currency</tt> + # * <tt>:nls_iso_currency</tt> + # * <tt>:nls_language</tt> + # * <tt>:nls_length_semantics</tt> - semantics of size of VARCHAR2 and CHAR columns, defaults to <tt>CHAR</tt> + # (meaning that size specifies number of characters and not bytes) + # * <tt>:nls_nchar_characterset</tt> + # * <tt>:nls_nchar_conv_excp</tt> + # * <tt>:nls_numeric_characters</tt> + # * <tt>:nls_sort</tt> + # * <tt>:nls_territory</tt> + # * <tt>:nls_timestamp_format</tt> - format for :timestamp columns, defaults to <tt>YYYY-MM-DD HH24:MI:SS:FF6</tt> + # * <tt>:nls_timestamp_tz_format</tt> + # * <tt>:nls_time_format</tt> + # * <tt>:nls_time_tz_format</tt> + # + class OracleEnhancedAdapter < AbstractAdapter + + + #sonar + # Callback to close the connection when HTTP request is processed + self.checkin :on_checkin + + def on_checkin + disconnect! + end + #/sonar + + ## + # :singleton-method: + # By default, the OracleEnhancedAdapter will consider all columns of type <tt>NUMBER(1)</tt> + # as boolean. If you wish to disable this emulation you can add the following line + # to your initializer file: + # + # ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.emulate_booleans = false + cattr_accessor :emulate_booleans + self.emulate_booleans = true + + ## + # :singleton-method: + # By default, the OracleEnhancedAdapter will typecast all columns of type <tt>DATE</tt> + # to Time or DateTime (if value is out of Time value range) value. + # If you wish that DATE values with hour, minutes and seconds equal to 0 are typecasted + # to Date then you can add the following line to your initializer file: + # + # ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.emulate_dates = true + # + # As this option can have side effects when unnecessary typecasting is done it is recommended + # that Date columns are explicily defined with +set_date_columns+ method. + cattr_accessor :emulate_dates + self.emulate_dates = false + + ## + # :singleton-method: + # OracleEnhancedAdapter will use the default tablespace, but if you want specific types of + # objects to go into specific tablespaces, specify them like this in an initializer: + # + # ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.default_tablespaces = + # {:clob => 'TS_LOB', :blob => 'TS_LOB', :index => 'TS_INDEX', :table => 'TS_DATA'} + # + # Using the :tablespace option where available (e.g create_table) will take precedence + # over these settings. + cattr_accessor :default_tablespaces + self.default_tablespaces={} + + ## + # :singleton-method: + # By default, the OracleEnhancedAdapter will typecast all columns of type <tt>DATE</tt> + # to Time or DateTime (if value is out of Time value range) value. + # If you wish that DATE columns with "date" in their name (e.g. "creation_date") are typecasted + # to Date then you can add the following line to your initializer file: + # + # ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.emulate_dates_by_column_name = true + # + # As this option can have side effects when unnecessary typecasting is done it is recommended + # that Date columns are explicily defined with +set_date_columns+ method. + cattr_accessor :emulate_dates_by_column_name + self.emulate_dates_by_column_name = false + + # Check column name to identify if it is Date (and not Time) column. + # Is used if +emulate_dates_by_column_name+ option is set to +true+. + # Override this method definition in initializer file if different Date column recognition is needed. + def self.is_date_column?(name, table_name = nil) + name =~ /(^|_)date(_|$)/i + end + + # instance method uses at first check if column type defined at class level + def is_date_column?(name, table_name = nil) #:nodoc: + case get_type_for_column(table_name, name) + when nil + self.class.is_date_column?(name, table_name) + when :date + true + else + false + end + end + + ## + # :singleton-method: + # By default, the OracleEnhancedAdapter will typecast all columns of type <tt>NUMBER</tt> + # (without precision or scale) to Float or BigDecimal value. + # If you wish that NUMBER columns with name "id" or that end with "_id" are typecasted + # to Integer then you can add the following line to your initializer file: + # + # ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.emulate_integers_by_column_name = true + cattr_accessor :emulate_integers_by_column_name + self.emulate_integers_by_column_name = false + + # Check column name to identify if it is Integer (and not Float or BigDecimal) column. + # Is used if +emulate_integers_by_column_name+ option is set to +true+. + # Override this method definition in initializer file if different Integer column recognition is needed. + def self.is_integer_column?(name, table_name = nil) + name =~ /(^|_)id$/i + end + + ## + # :singleton-method: + # If you wish that CHAR(1), VARCHAR2(1) columns or VARCHAR2 columns with FLAG or YN at the end of their name + # are typecasted to booleans then you can add the following line to your initializer file: + # + # ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.emulate_booleans_from_strings = true + cattr_accessor :emulate_booleans_from_strings + self.emulate_booleans_from_strings = false + + # Check column name to identify if it is boolean (and not String) column. + # Is used if +emulate_booleans_from_strings+ option is set to +true+. + # Override this method definition in initializer file if different boolean column recognition is needed. + def self.is_boolean_column?(name, field_type, table_name = nil) + return true if ["CHAR(1)","VARCHAR2(1)"].include?(field_type) + field_type =~ /^VARCHAR2/ && (name =~ /_flag$/i || name =~ /_yn$/i) + end + + # How boolean value should be quoted to String. + # Used if +emulate_booleans_from_strings+ option is set to +true+. + def self.boolean_to_string(bool) + bool ? "Y" : "N" + end + + ## + # :singleton-method: + # Specify non-default date format that should be used when assigning string values to :date columns, e.g.: + # + # ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.string_to_date_format = “%d.%m.%Y†+ cattr_accessor :string_to_date_format + self.string_to_date_format = nil + + ## + # :singleton-method: + # Specify non-default time format that should be used when assigning string values to :datetime columns, e.g.: + # + # ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.string_to_time_format = “%d.%m.%Y %H:%M:%S†+ cattr_accessor :string_to_time_format + self.string_to_time_format = nil + + def initialize(connection, logger = nil) #:nodoc: + super + @quoted_column_names, @quoted_table_names = {}, {} + @statements = {} + @enable_dbms_output = false + end + + def self.visitor_for(pool) # :nodoc: + Arel::Visitors::Oracle.new(pool) + end + + ADAPTER_NAME = 'OracleEnhanced'.freeze + + def adapter_name #:nodoc: + ADAPTER_NAME + end + + def supports_migrations? #:nodoc: + true + end + + def supports_primary_key? #:nodoc: + true + end + + def supports_savepoints? #:nodoc: + true + end + + #:stopdoc: + DEFAULT_NLS_PARAMETERS = { + :nls_calendar => nil, + :nls_characterset => nil, + :nls_comp => nil, + :nls_currency => nil, + :nls_date_format => 'YYYY-MM-DD HH24:MI:SS', + :nls_date_language => nil, + :nls_dual_currency => nil, + :nls_iso_currency => nil, + :nls_language => nil, + :nls_length_semantics => 'CHAR', + :nls_nchar_characterset => nil, + :nls_nchar_conv_excp => nil, + :nls_numeric_characters => nil, + :nls_sort => nil, + :nls_territory => nil, + :nls_timestamp_format => 'YYYY-MM-DD HH24:MI:SS:FF6', + :nls_timestamp_tz_format => nil, + :nls_time_format => nil, + :nls_time_tz_format => nil + } + + #:stopdoc: + NATIVE_DATABASE_TYPES = { + :primary_key => "NUMBER(38) NOT NULL PRIMARY KEY", + :string => { :name => "VARCHAR2", :limit => 255 }, + :text => { :name => "CLOB" }, + :integer => { :name => "NUMBER", :limit => 38 }, + :float => { :name => "NUMBER" }, + :decimal => { :name => "DECIMAL" }, + # sonar + :datetime => { :name => "TIMESTAMP" }, + # /sonar + # changed to native TIMESTAMP type + # :timestamp => { :name => "DATE" }, + :timestamp => { :name => "TIMESTAMP" }, + :time => { :name => "DATE" }, + :date => { :name => "DATE" }, + :binary => { :name => "BLOB" }, + :boolean => { :name => "NUMBER", :limit => 1 }, + :raw => { :name => "RAW", :limit => 2000 }, + + #sonar - big integers are exactly integers + :big_integer => { :name => "NUMBER", :limit => 38 } + #/sonar + } + # if emulate_booleans_from_strings then store booleans in VARCHAR2 + NATIVE_DATABASE_TYPES_BOOLEAN_STRINGS = NATIVE_DATABASE_TYPES.dup.merge( + :boolean => { :name => "VARCHAR2", :limit => 1 } + ) + #:startdoc: + + def native_database_types #:nodoc: + emulate_booleans_from_strings ? NATIVE_DATABASE_TYPES_BOOLEAN_STRINGS : NATIVE_DATABASE_TYPES + end + + # maximum length of Oracle identifiers + IDENTIFIER_MAX_LENGTH = 30 + + def table_alias_length #:nodoc: + IDENTIFIER_MAX_LENGTH + end + + # the maximum length of a table name + def table_name_length + IDENTIFIER_MAX_LENGTH + end + + # the maximum length of a column name + def column_name_length + IDENTIFIER_MAX_LENGTH + end + + # the maximum length of an index name + def index_name_length + IDENTIFIER_MAX_LENGTH + end + + # To avoid ORA-01795: maximum number of expressions in a list is 1000 + # tell ActiveRecord to limit us to 1000 ids at a time + def in_clause_length + 1000 + end + alias ids_in_list_limit in_clause_length + + # QUOTING ================================================== + # + # see: abstract/quoting.rb + + def quote_column_name(name) #:nodoc: + name = name.to_s + @quoted_column_names[name] ||= begin + # if only valid lowercase column characters in name + if name =~ /\A[a-z][a-z_0-9\$#]*\Z/ + "\"#{name.upcase}\"" + else + # remove double quotes which cannot be used inside quoted identifier + "\"#{name.gsub('"', '')}\"" + end + end + end + + # This method is used in add_index to identify either column name (which is quoted) + # or function based index (in which case function expression is not quoted) + def quote_column_name_or_expression(name) #:nodoc: + name = name.to_s + case name + # if only valid lowercase column characters in name + when /^[a-z][a-z_0-9\$#]*$/ + "\"#{name.upcase}\"" + when /^[a-z][a-z_0-9\$#\-]*$/i + "\"#{name}\"" + # if other characters present then assume that it is expression + # which should not be quoted + else + name + end + end + + # Names must be from 1 to 30 bytes long with these exceptions: + # * Names of databases are limited to 8 bytes. + # * Names of database links can be as long as 128 bytes. + # + # Nonquoted identifiers cannot be Oracle Database reserved words + # + # Nonquoted identifiers must begin with an alphabetic character from + # your database character set + # + # Nonquoted identifiers can contain only alphanumeric characters from + # your database character set and the underscore (_), dollar sign ($), + # and pound sign (#). Database links can also contain periods (.) and + # "at" signs (@). Oracle strongly discourages you from using $ and # in + # nonquoted identifiers. + NONQUOTED_OBJECT_NAME = /[A-Za-z][A-z0-9$#]{0,29}/ + NONQUOTED_DATABASE_LINK = /[A-Za-z][A-z0-9$#\.@]{0,127}/ + VALID_TABLE_NAME = /\A(?:#{NONQUOTED_OBJECT_NAME}\.)?#{NONQUOTED_OBJECT_NAME}(?:@#{NONQUOTED_DATABASE_LINK})?\Z/ + + # unescaped table name should start with letter and + # contain letters, digits, _, $ or # + # can be prefixed with schema name + # CamelCase table names should be quoted + def self.valid_table_name?(name) #:nodoc: + name = name.to_s + name =~ VALID_TABLE_NAME && !(name =~ /[A-Z]/ && name =~ /[a-z]/) ? true : false + end + + def quote_table_name(name) #:nodoc: + name = name.to_s + @quoted_table_names[name] ||= name.split('.').map{|n| n.split('@').map{|m| quote_column_name(m)}.join('@')}.join('.') + end + + def quote_string(s) #:nodoc: + s.gsub(/'/, "''") + end + + def quote(value, column = nil) #:nodoc: + if value && column + case column.type + when :text, :binary + %Q{empty_#{ column.sql_type.downcase rescue 'blob' }()} + # NLS_DATE_FORMAT independent TIMESTAMP support + when :timestamp + quote_timestamp_with_to_timestamp(value) + # NLS_DATE_FORMAT independent DATE support + when :date, :time, :datetime + quote_date_with_to_date(value) + when :raw + quote_raw(value) + when :string + # NCHAR and NVARCHAR2 literals should be quoted with N'...'. + # Read directly instance variable as otherwise migrations with table column default values are failing + # as migrations pass ColumnDefinition object to this method. + # Check if instance variable is defined to avoid warnings about accessing undefined instance variable. + column.instance_variable_defined?('@nchar') && column.instance_variable_get('@nchar') ? 'N' << super : super + else + super + end + elsif value.acts_like?(:date) + quote_date_with_to_date(value) + elsif value.acts_like?(:time) + value.to_i == value.to_f ? quote_date_with_to_date(value) : quote_timestamp_with_to_timestamp(value) + else + super + end + end + + def quoted_true #:nodoc: + return "'#{self.class.boolean_to_string(true)}'" if emulate_booleans_from_strings + "1" + end + + def quoted_false #:nodoc: + return "'#{self.class.boolean_to_string(false)}'" if emulate_booleans_from_strings + "0" + end + + def quote_date_with_to_date(value) #:nodoc: + # should support that composite_primary_keys gem will pass date as string + value = quoted_date(value) if value.acts_like?(:date) || value.acts_like?(:time) + "TO_DATE('#{value}','YYYY-MM-DD HH24:MI:SS')" + end + + # Encode a string or byte array as string of hex codes + def self.encode_raw(value) + # When given a string, convert to a byte array. + value = value.unpack('C*') if value.is_a?(String) + value.map { |x| "%02X" % x }.join + end + + # quote encoded raw value + def quote_raw(value) #:nodoc: + "'#{self.class.encode_raw(value)}'" + end + + def quote_timestamp_with_to_timestamp(value) #:nodoc: + # add up to 9 digits of fractional seconds to inserted time + value = "#{quoted_date(value)}:#{("%.6f"%value.to_f).split('.')[1]}" if value.acts_like?(:time) + "TO_TIMESTAMP('#{value}','YYYY-MM-DD HH24:MI:SS:FF6')" + end + + # Cast a +value+ to a type that the database understands. + def type_cast(value, column) + case value + when true, false + if emulate_booleans_from_strings || column && column.type == :string + self.class.boolean_to_string(value) + else + value ? 1 : 0 + end + when Date, Time + if value.acts_like?(:time) + zone_conversion_method = ActiveRecord::Base.default_timezone == :utc ? :getutc : :getlocal + value.respond_to?(zone_conversion_method) ? value.send(zone_conversion_method) : value + else + value + end + else + super + end + end + + # CONNECTION MANAGEMENT ==================================== + # + + # If SQL statement fails due to lost connection then reconnect + # and retry SQL statement if autocommit mode is enabled. + # By default this functionality is disabled. + attr_reader :auto_retry #:nodoc: + @auto_retry = false + + def auto_retry=(value) #:nodoc: + @auto_retry = value + @connection.auto_retry = value if @connection + end + + # return raw OCI8 or JDBC connection + def raw_connection + @connection.raw_connection + end + + # Returns true if the connection is active. + def active? #:nodoc: + # Pings the connection to check if it's still good. Note that an + # #active? method is also available, but that simply returns the + # last known state, which isn't good enough if the connection has + # gone stale since the last use. + @connection.ping + rescue OracleEnhancedConnectionException + false + end + + # Reconnects to the database. + def reconnect! #:nodoc: + clear_cache! + @connection.reset! + rescue OracleEnhancedConnectionException => e + @logger.warn "#{adapter_name} automatic reconnection failed: #{e.message}" if @logger + end + + def reset! + clear_cache! + super + end + + # Disconnects from the database. + def disconnect! #:nodoc: + clear_cache! + @connection.logoff rescue nil + end + + # DATABASE STATEMENTS ====================================== + # + # see: abstract/database_statements.rb + + # Executes a SQL statement + def execute(sql, name = nil) + log(sql, name) { @connection.exec(sql) } + end + + def substitute_at(column, index) + Arel.sql(":a#{index + 1}") + end + + def clear_cache! + @statements.each_value do |cursor| + cursor.close + end + @statements.clear + end + + def exec_query(sql, name = 'SQL', binds = []) + log(sql, name, binds) do + cursor = nil + cached = false + if binds.empty? + cursor = @connection.prepare(sql) + else + unless @statements.key? sql + @statements[sql] = @connection.prepare(sql) + end + + cursor = @statements[sql] + + binds.each_with_index do |bind, i| + col, val = bind + cursor.bind_param(i + 1, type_cast(val, col), col && col.type) + end + + cached = true + end + + cursor.exec + columns = cursor.get_col_names.map do |col_name| + @connection.oracle_downcase(col_name) + end + rows = [] + fetch_options = {:get_lob_value => (name != 'Writable Large Object')} + while row = cursor.fetch(fetch_options) + rows << row + end + res = ActiveRecord::Result.new(columns, rows) + cursor.close unless cached + res + end + end + + def supports_statement_cache? + true + end + + # Returns an array of arrays containing the field values. + # Order is the same as that returned by #columns. + def select_rows(sql, name = nil) + # last parameter indicates to return also column list + result = columns = nil + log(sql, name) do + result, columns = @connection.select(sql, name, true) + end + result.map{ |v| columns.map{|c| v[c]} } + end + + # Executes an INSERT statement and returns the new record's ID + def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) #:nodoc: + # if primary key value is already prefetched from sequence + # or if there is no primary key + if id_value || pk.nil? + execute(sql, name) + return id_value + end + + sql_with_returning = sql + @connection.returning_clause(quote_column_name(pk)) + log(sql, name) do + @connection.exec_with_returning(sql_with_returning) + end + end + protected :insert_sql + + # New method in ActiveRecord 3.1 + # Will add RETURNING clause in case of trigger generated primary keys + def sql_for_insert(sql, pk, id_value, sequence_name, binds) + unless id_value || pk.nil? + sql = "#{sql} RETURNING #{quote_column_name(pk)} INTO :returning_id" + (binds = binds.dup) << [:returning_id, nil] + end + [sql, binds] + end + + EXEC_INSERT_RESULT_COLUMNS = %w(returning_id) #:nodoc: + + # New method in ActiveRecord 3.1 + def exec_insert(sql, name, binds) + log(sql, name, binds) do + returning_id_index = nil + cursor = if @statements.key?(sql) + @statements[sql] + else + @statements[sql] = @connection.prepare(sql) + end + + binds.each_with_index do |bind, i| + col, val = bind + if col == :returning_id + returning_id_index = i + 1 + cursor.bind_returning_param(returning_id_index, Integer) + else + cursor.bind_param(i + 1, type_cast(val, col), col && col.type) + end + end + + cursor.exec_update + + rows = [] + if returning_id_index + returning_id = cursor.get_returning_param(returning_id_index, Integer) + rows << [returning_id] + end + ActiveRecord::Result.new(EXEC_INSERT_RESULT_COLUMNS, rows) + end + end + + # New method in ActiveRecord 3.1 + def exec_update(sql, name, binds) + log(sql, name, binds) do + cursor = if @statements.key?(sql) + @statements[sql] + else + @statements[sql] = @connection.prepare(sql) + end + + binds.each_with_index do |bind, i| + col, val = bind + cursor.bind_param(i + 1, type_cast(val, col), col && col.type) + end + + cursor.exec_update + end + end + + alias :exec_delete :exec_update + + # use in set_sequence_name to avoid fetching primary key value from sequence + AUTOGENERATED_SEQUENCE_NAME = 'autogenerated'.freeze + + # Returns the next sequence value from a sequence generator. Not generally + # called directly; used by ActiveRecord to get the next primary key value + # when inserting a new database record (see #prefetch_primary_key?). + def next_sequence_value(sequence_name) + # if sequence_name is set to :autogenerated then it means that primary key will be populated by trigger + return nil if sequence_name == AUTOGENERATED_SEQUENCE_NAME + # call directly connection method to avoid prepared statement which causes fetching of next sequence value twice + @connection.select_value("SELECT #{quote_table_name(sequence_name)}.NEXTVAL FROM dual") + end + + def begin_db_transaction #:nodoc: + @connection.autocommit = false + end + + def commit_db_transaction #:nodoc: + @connection.commit + ensure + @connection.autocommit = true + end + + def rollback_db_transaction #:nodoc: + @connection.rollback + ensure + @connection.autocommit = true + end + + def create_savepoint #:nodoc: + execute("SAVEPOINT #{current_savepoint_name}") + end + + def rollback_to_savepoint #:nodoc: + execute("ROLLBACK TO #{current_savepoint_name}") + end + + def release_savepoint #:nodoc: + # there is no RELEASE SAVEPOINT statement in Oracle + end + + def add_limit_offset!(sql, options) #:nodoc: + # added to_i for limit and offset to protect from SQL injection + offset = (options[:offset] || 0).to_i + limit = options[:limit] + limit = limit.is_a?(String) && limit.blank? ? nil : limit && limit.to_i + if limit && offset > 0 + sql.replace "SELECT * FROM (SELECT raw_sql_.*, ROWNUM raw_rnum_ FROM (#{sql}) raw_sql_ WHERE ROWNUM <= #{offset+limit}) WHERE raw_rnum_ > #{offset}" + elsif limit + sql.replace "SELECT * FROM (#{sql}) WHERE ROWNUM <= #{limit}" + elsif offset > 0 + sql.replace "SELECT * FROM (SELECT raw_sql_.*, ROWNUM raw_rnum_ FROM (#{sql}) raw_sql_) WHERE raw_rnum_ > #{offset}" + end + end + + @@do_not_prefetch_primary_key = {} + + # Returns true for Oracle adapter (since Oracle requires primary key + # values to be pre-fetched before insert). See also #next_sequence_value. + def prefetch_primary_key?(table_name = nil) + return true if table_name.nil? + table_name = table_name.to_s + do_not_prefetch = @@do_not_prefetch_primary_key[table_name] + if do_not_prefetch.nil? + owner, desc_table_name, db_link = @connection.describe(table_name) + @@do_not_prefetch_primary_key[table_name] = do_not_prefetch = + !has_primary_key?(table_name, owner, desc_table_name, db_link) || + has_primary_key_trigger?(table_name, owner, desc_table_name, db_link) + end + !do_not_prefetch + end + + # used just in tests to clear prefetch primary key flag for all tables + def clear_prefetch_primary_key #:nodoc: + @@do_not_prefetch_primary_key = {} + end + + # Returns default sequence name for table. + # Will take all or first 26 characters of table name and append _seq suffix + def default_sequence_name(table_name, primary_key = nil) + # TODO: remove schema prefix if present before truncating + # truncate table name if necessary to fit in max length of identifier + "#{table_name.to_s[0,IDENTIFIER_MAX_LENGTH-4]}_seq" + end + + # Inserts the given fixture into the table. Overridden to properly handle lobs. + def insert_fixture(fixture, table_name) #:nodoc: + super + + if ActiveRecord::Base.pluralize_table_names + klass = table_name.singularize.camelize + else + klass = table_name.camelize + end + + klass = klass.constantize rescue nil + if klass.respond_to?(:ancestors) && klass.ancestors.include?(ActiveRecord::Base) + write_lobs(table_name, klass, fixture) + end + end + + # Writes LOB values from attributes, as indicated by the LOB columns of klass. + def write_lobs(table_name, klass, attributes) #:nodoc: + # is class with composite primary key> + is_with_cpk = klass.respond_to?(:composite?) && klass.composite? + if is_with_cpk + id = klass.primary_key.map {|pk| attributes[pk.to_s] } + else + id = quote(attributes[klass.primary_key]) + end + klass.columns.select { |col| col.sql_type =~ /LOB$/i }.each do |col| + value = attributes[col.name] + # changed sequence of next two lines - should check if value is nil before converting to yaml + next if value.nil? || (value == '') + value = value.to_yaml if col.text? && klass.serialized_attributes[col.name] + uncached do + sql = is_with_cpk ? "SELECT #{quote_column_name(col.name)} FROM #{quote_table_name(table_name)} WHERE #{klass.composite_where_clause(id)} FOR UPDATE" : + "SELECT #{quote_column_name(col.name)} FROM #{quote_table_name(table_name)} WHERE #{quote_column_name(klass.primary_key)} = #{id} FOR UPDATE" + unless lob_record = select_one(sql, 'Writable Large Object') + raise ActiveRecord::RecordNotFound, "statement #{sql} returned no rows" + end + lob = lob_record[col.name] + @connection.write_lob(lob, value.to_s, col.type == :binary) + end + end + end + + # Current database name + def current_database + select_value("SELECT SYS_CONTEXT('userenv', 'db_name') FROM dual") + end + + # Current database session user + def current_user + select_value("SELECT SYS_CONTEXT('userenv', 'session_user') FROM dual") + end + + # Default tablespace name of current user + def default_tablespace + select_value("SELECT LOWER(default_tablespace) FROM user_users WHERE username = SYS_CONTEXT('userenv', 'session_user')") + end + + def tables(name = nil) #:nodoc: + select_values( + "SELECT DECODE(table_name, UPPER(table_name), LOWER(table_name), table_name) FROM all_tables WHERE owner = SYS_CONTEXT('userenv', 'session_user') AND secondary = 'N'", + name) + end + + # Will return true if database object exists (to be able to use also views and synonyms for ActiveRecord models) + def table_exists?(table_name) + (owner, table_name, db_link) = @connection.describe(table_name) + true + rescue + false + end + + def materialized_views #:nodoc: + select_values("SELECT LOWER(mview_name) FROM all_mviews WHERE owner = SYS_CONTEXT('userenv', 'session_user')") + end + + cattr_accessor :all_schema_indexes #:nodoc: + + # This method selects all indexes at once, and caches them in a class variable. + # Subsequent index calls get them from the variable, without going to the DB. + def indexes(table_name, name = nil) #:nodoc: + (owner, table_name, db_link) = @connection.describe(table_name) + unless all_schema_indexes + default_tablespace_name = default_tablespace + result = select_all(<<-SQL.strip.gsub(/\s+/, ' ')) + SELECT LOWER(i.table_name) AS table_name, LOWER(i.index_name) AS index_name, i.uniqueness, + i.index_type, i.ityp_owner, i.ityp_name, i.parameters, + LOWER(i.tablespace_name) AS tablespace_name, + LOWER(c.column_name) AS column_name, e.column_expression + FROM all_indexes#{db_link} i + JOIN all_ind_columns#{db_link} c ON c.index_name = i.index_name AND c.index_owner = i.owner + LEFT OUTER JOIN all_ind_expressions#{db_link} e ON e.index_name = i.index_name AND + e.index_owner = i.owner AND e.column_position = c.column_position + WHERE i.owner = '#{owner}' + AND i.table_owner = '#{owner}' + AND NOT EXISTS (SELECT uc.index_name FROM all_constraints uc + WHERE uc.index_name = i.index_name AND uc.owner = i.owner AND uc.constraint_type = 'P') + ORDER BY i.index_name, c.column_position + SQL + + current_index = nil + self.all_schema_indexes = [] + + result.each do |row| + # have to keep track of indexes because above query returns dups + # there is probably a better query we could figure out + if current_index != row['index_name'] + statement_parameters = nil + if row['index_type'] == 'DOMAIN' && row['ityp_owner'] == 'CTXSYS' && row['ityp_name'] == 'CONTEXT' + procedure_name = default_datastore_procedure(row['index_name']) + source = select_values(<<-SQL).join + SELECT text + FROM all_source#{db_link} + WHERE owner = '#{owner}' + AND name = '#{procedure_name.upcase}' + ORDER BY line + SQL + if source =~ /-- add_context_index_parameters (.+)\n/ + statement_parameters = $1 + end + end + all_schema_indexes << OracleEnhancedIndexDefinition.new(row['table_name'], row['index_name'], + row['uniqueness'] == "UNIQUE", row['index_type'] == 'DOMAIN' ? "#{row['ityp_owner']}.#{row['ityp_name']}" : nil, + row['parameters'], statement_parameters, + row['tablespace_name'] == default_tablespace_name ? nil : row['tablespace_name'], []) + current_index = row['index_name'] + end + all_schema_indexes.last.columns << (row['column_expression'] || row['column_name'].downcase) + end + end + + # Return the indexes just for the requested table, since AR is structured that way + table_name = table_name.downcase + all_schema_indexes.select{|i| i.table == table_name} + end + + @@ignore_table_columns = nil #:nodoc: + + # set ignored columns for table + def ignore_table_columns(table_name, *args) #:nodoc: + @@ignore_table_columns ||= {} + @@ignore_table_columns[table_name] ||= [] + @@ignore_table_columns[table_name] += args.map{|a| a.to_s.downcase} + @@ignore_table_columns[table_name].uniq! + end + + def ignored_table_columns(table_name) #:nodoc: + @@ignore_table_columns ||= {} + @@ignore_table_columns[table_name] + end + + # used just in tests to clear ignored table columns + def clear_ignored_table_columns #:nodoc: + @@ignore_table_columns = nil + end + + @@table_column_type = nil #:nodoc: + + # set explicit type for specified table columns + def set_type_for_columns(table_name, column_type, *args) #:nodoc: + @@table_column_type ||= {} + @@table_column_type[table_name] ||= {} + args.each do |col| + @@table_column_type[table_name][col.to_s.downcase] = column_type + end + end + + def get_type_for_column(table_name, column_name) #:nodoc: + @@table_column_type && @@table_column_type[table_name] && @@table_column_type[table_name][column_name.to_s.downcase] + end + + # used just in tests to clear column data type definitions + def clear_types_for_columns #:nodoc: + @@table_column_type = nil + end + + # check if table has primary key trigger with _pkt suffix + def has_primary_key_trigger?(table_name, owner = nil, desc_table_name = nil, db_link = nil) + (owner, desc_table_name, db_link) = @connection.describe(table_name) unless owner + + trigger_name = default_trigger_name(table_name).upcase + pkt_sql = <<-SQL + SELECT trigger_name + FROM all_triggers#{db_link} + WHERE owner = '#{owner}' + AND trigger_name = '#{trigger_name}' + AND table_owner = '#{owner}' + AND table_name = '#{desc_table_name}' + AND status = 'ENABLED' + SQL + select_value(pkt_sql, 'Primary Key Trigger') ? true : false + end + + ## + # :singleton-method: + # Cache column description between requests. + # Could be used in development environment to avoid selecting table columns from data dictionary tables for each request. + # This can speed up request processing in development mode if development database is not on local computer. + # + # ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.cache_columns = true + cattr_accessor :cache_columns + self.cache_columns = false + + def columns(table_name, name = nil) #:nodoc: + if @@cache_columns + @@columns_cache ||= {} + @@columns_cache[table_name] ||= columns_without_cache(table_name, name) + else + columns_without_cache(table_name, name) + end + end + + def columns_without_cache(table_name, name = nil) #:nodoc: + table_name = table_name.to_s + # get ignored_columns by original table name + ignored_columns = ignored_table_columns(table_name) + + (owner, desc_table_name, db_link) = @connection.describe(table_name) + + # reset do_not_prefetch_primary_key cache for this table + @@do_not_prefetch_primary_key[table_name] = nil + + table_cols = <<-SQL.strip.gsub(/\s+/, ' ') + SELECT column_name AS name, data_type AS sql_type, data_default, nullable, virtual_column, hidden_column, + DECODE(data_type, 'NUMBER', data_precision, + 'FLOAT', data_precision, + 'VARCHAR2', DECODE(char_used, 'C', char_length, data_length), + 'RAW', DECODE(char_used, 'C', char_length, data_length), + 'CHAR', DECODE(char_used, 'C', char_length, data_length), + NULL) AS limit, + DECODE(data_type, 'NUMBER', data_scale, NULL) AS scale + FROM all_tab_cols#{db_link} + WHERE owner = '#{owner}' + AND table_name = '#{desc_table_name}' + AND hidden_column = 'NO' + ORDER BY column_id + SQL + + # added deletion of ignored columns + select_all(table_cols, name).delete_if do |row| + ignored_columns && ignored_columns.include?(row['name'].downcase) + end.map do |row| + limit, scale = row['limit'], row['scale'] + if limit || scale + row['sql_type'] += "(#{(limit || 38).to_i}" + ((scale = scale.to_i) > 0 ? ",#{scale})" : ")") + end + + # clean up odd default spacing from Oracle + if row['data_default'] + row['data_default'].sub!(/^(.*?)\s*$/, '\1') + + # If a default contains a newline these cleanup regexes need to + # match newlines. + row['data_default'].sub!(/^'(.*)'$/m, '\1') + row['data_default'] = nil if row['data_default'] =~ /^(null|empty_[bc]lob\(\))$/i + end + + OracleEnhancedColumn.new(oracle_downcase(row['name']), + row['data_default'], + row['sql_type'], + row['nullable'] == 'Y', + # pass table name for table specific column definitions + table_name, + # pass column type if specified in class definition + get_type_for_column(table_name, oracle_downcase(row['name'])), row['virtual_column']=='YES') + end + end + + # used just in tests to clear column cache + def clear_columns_cache #:nodoc: + @@columns_cache = nil + @@pk_and_sequence_for_cache = nil + end + + # used in migrations to clear column cache for specified table + def clear_table_columns_cache(table_name) + if @@cache_columns + @@columns_cache ||= {} + @@columns_cache[table_name.to_s] = nil + end + end + + ## + # :singleton-method: + # Specify default sequence start with value (by default 10000 if not explicitly set), e.g.: + # + # ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.default_sequence_start_value = 1 + cattr_accessor :default_sequence_start_value + self.default_sequence_start_value = 10000 + + # Find a table's primary key and sequence. + # *Note*: Only primary key is implemented - sequence will be nil. + def pk_and_sequence_for(table_name, owner=nil, desc_table_name=nil, db_link=nil) #:nodoc: + if @@cache_columns + @@pk_and_sequence_for_cache ||= {} + @@pk_and_sequence_for_cache[table_name] ||= pk_and_sequence_for_without_cache(table_name, owner, desc_table_name, db_link) + else + pk_and_sequence_for_without_cache(table_name, owner, desc_table_name, db_link) + end + end + + def pk_and_sequence_for_without_cache(table_name, owner=nil, desc_table_name=nil, db_link=nil) #:nodoc: + (owner, desc_table_name, db_link) = @connection.describe(table_name) unless owner + + # changed back from user_constraints to all_constraints for consistency + pks = select_values(<<-SQL.strip.gsub(/\s+/, ' '), 'Primary Key') + SELECT cc.column_name + FROM all_constraints#{db_link} c, all_cons_columns#{db_link} cc + WHERE c.owner = '#{owner}' + AND c.table_name = '#{desc_table_name}' + AND c.constraint_type = 'P' + AND cc.owner = c.owner + AND cc.constraint_name = c.constraint_name + SQL + + # only support single column keys + pks.size == 1 ? [oracle_downcase(pks.first), nil] : nil + end + + # Returns just a table's primary key + def primary_key(table_name) + pk_and_sequence = pk_and_sequence_for(table_name) + pk_and_sequence && pk_and_sequence.first + end + + def has_primary_key?(table_name, owner=nil, desc_table_name=nil, db_link=nil) #:nodoc: + !pk_and_sequence_for(table_name, owner, desc_table_name, db_link).nil? + end + + # SELECT DISTINCT clause for a given set of columns and a given ORDER BY clause. + # + # Oracle requires the ORDER BY columns to be in the SELECT list for DISTINCT + # queries. However, with those columns included in the SELECT DISTINCT list, you + # won't actually get a distinct list of the column you want (presuming the column + # has duplicates with multiple values for the ordered-by columns. So we use the + # FIRST_VALUE function to get a single (first) value for each column, effectively + # making every row the same. + # + # distinct("posts.id", "posts.created_at desc") + def distinct(columns, order_by) #:nodoc: + return "DISTINCT #{columns}" if order_by.blank? + + # construct a valid DISTINCT clause, ie. one that includes the ORDER BY columns, using + # FIRST_VALUE such that the inclusion of these columns doesn't invalidate the DISTINCT + order_columns = if order_by.is_a?(String) + order_by.split(',').map { |s| s.strip }.reject(&:blank?) + else # in latest ActiveRecord versions order_by is already Array + order_by + end + order_columns = order_columns.zip((0...order_columns.size).to_a).map do |c, i| + # remove any ASC/DESC modifiers + value = c =~ /^(.+)\s+(ASC|DESC)\s*$/i ? $1 : c + "FIRST_VALUE(#{value}) OVER (PARTITION BY #{columns} ORDER BY #{c}) AS alias_#{i}__" + end + sql = "DISTINCT #{columns}, " + sql << order_columns * ", " + end + + def temporary_table?(table_name) #:nodoc: + select_value("SELECT temporary FROM user_tables WHERE table_name = '#{table_name.upcase}'") == 'Y' + end + + # ORDER BY clause for the passed order option. + # + # Uses column aliases as defined by #distinct. + # + # In Rails 3.x this method is moved to Arel + def add_order_by_for_association_limiting!(sql, options) #:nodoc: + return sql if options[:order].blank? + + order = options[:order].split(',').collect { |s| s.strip }.reject(&:blank?) + order.map! {|s| $1 if s =~ / (.*)/} + order = order.zip((0...order.size).to_a).map { |s,i| "alias_#{i}__ #{s}" }.join(', ') + + sql << " ORDER BY #{order}" + end + + protected + + def translate_exception(exception, message) #:nodoc: + case @connection.error_code(exception) + when 1 + RecordNotUnique.new(message, exception) + when 2291 + InvalidForeignKey.new(message, exception) + else + super + end + end + + private + + def select(sql, name = nil, binds = []) + if ActiveRecord.const_defined?(:Result) + exec_query(sql, name, binds).to_a + else + log(sql, name) do + @connection.select(sql, name, false) + end + end + end + + def oracle_downcase(column_name) + @connection.oracle_downcase(column_name) + end + + def compress_lines(string, join_with = "\n") + string.split($/).map { |line| line.strip }.join(join_with) + end + + public + # DBMS_OUTPUT ============================================= + # + # PL/SQL in Oracle uses dbms_output for logging print statements + # These methods stick that output into the Rails log so Ruby and PL/SQL + # code can can be debugged together in a single application + + # Maximum DBMS_OUTPUT buffer size + DBMS_OUTPUT_BUFFER_SIZE = 10000 # can be 1-1000000 + + # Turn DBMS_Output logging on + def enable_dbms_output + set_dbms_output_plsql_connection + @enable_dbms_output = true + plsql(:dbms_output).sys.dbms_output.enable(DBMS_OUTPUT_BUFFER_SIZE) + end + # Turn DBMS_Output logging off + def disable_dbms_output + set_dbms_output_plsql_connection + @enable_dbms_output = false + plsql(:dbms_output).sys.dbms_output.disable + end + # Is DBMS_Output logging enabled? + def dbms_output_enabled? + @enable_dbms_output + end + + protected + def log(sql, name, binds = nil) #:nodoc: + if binds + super sql, name, binds + else + super sql, name + end + ensure + log_dbms_output if dbms_output_enabled? + end + + private + + def set_dbms_output_plsql_connection + raise OracleEnhancedConnectionException, "ruby-plsql gem is required for logging DBMS output" unless self.respond_to?(:plsql) + # do not reset plsql connection if it is the same (as resetting will clear PL/SQL metadata cache) + unless plsql(:dbms_output).connection && plsql(:dbms_output).connection.raw_connection == raw_connection + plsql(:dbms_output).connection = raw_connection + end + end + + def log_dbms_output + while true do + result = plsql(:dbms_output).sys.dbms_output.get_line(:line => '', :status => 0) + break unless result[:status] == 0 + @logger.debug "DBMS_OUTPUT: #{result[:line]}" if @logger + end + end + + end + end +end + +# Added LOB writing callback for sessions stored in database +# Otherwise it is not working as Session class is defined before OracleAdapter is loaded in Rails 2.0 +if defined?(CGI::Session::ActiveRecordStore::Session) + if !CGI::Session::ActiveRecordStore::Session.respond_to?(:after_save_callback_chain) || + CGI::Session::ActiveRecordStore::Session.after_save_callback_chain.detect{|cb| cb.method == :enhanced_write_lobs}.nil? + #:stopdoc: + class CGI::Session::ActiveRecordStore::Session + after_save :enhanced_write_lobs + end + #:startdoc: + end +end + +# Implementation of standard schema definition statements and extensions for schema definition +require 'active_record/connection_adapters/oracle_enhanced_schema_statements' +require 'active_record/connection_adapters/oracle_enhanced_schema_statements_ext' + +# Extensions for schema definition +require 'active_record/connection_adapters/oracle_enhanced_schema_definitions' + +# Extensions for context index definition +require 'active_record/connection_adapters/oracle_enhanced_context_index' + +# Load custom create, update, delete methods functionality +require 'active_record/connection_adapters/oracle_enhanced_procedures' + +# Load additional methods for composite_primary_keys support +require 'active_record/connection_adapters/oracle_enhanced_cpk' + +# Load patch for dirty tracking methods +require 'active_record/connection_adapters/oracle_enhanced_dirty' + +# Load rake tasks definitions +begin + require 'active_record/connection_adapters/oracle_enhanced_tasks' +rescue LoadError +end if defined?(Rails) || defined?(RAILS_ROOT) + +# Patches and enhancements for schema dumper +require 'active_record/connection_adapters/oracle_enhanced_schema_dumper' + +# Implementation of structure dump +require 'active_record/connection_adapters/oracle_enhanced_structure_dump' + +# Add BigDecimal#to_d, Fixnum#to_d and Bignum#to_d methods if not already present +require 'active_record/connection_adapters/oracle_enhanced_core_ext' + +require 'active_record/connection_adapters/oracle_enhanced_activerecord_patches' + +require 'active_record/connection_adapters/oracle_enhanced_version' diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-oracle_enhanced-adapter-1.4.0/lib/active_record/connection_adapters/oracle_enhanced_base_ext.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-oracle_enhanced-adapter-1.4.0/lib/active_record/connection_adapters/oracle_enhanced_base_ext.rb new file mode 100644 index 00000000000..b9a2f533b8e --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-oracle_enhanced-adapter-1.4.0/lib/active_record/connection_adapters/oracle_enhanced_base_ext.rb @@ -0,0 +1,100 @@ +module ActiveRecord + class Base + # Establishes a connection to the database that's used by all Active Record objects. + def self.oracle_enhanced_connection(config) #:nodoc: + if config[:emulate_oracle_adapter] == true + # allows the enhanced adapter to look like the OracleAdapter. Useful to pick up + # conditionals in the rails activerecord test suite + require 'active_record/connection_adapters/emulation/oracle_adapter' + ConnectionAdapters::OracleAdapter.new( + ConnectionAdapters::OracleEnhancedConnection.create(config), logger) + else + ConnectionAdapters::OracleEnhancedAdapter.new( + ConnectionAdapters::OracleEnhancedConnection.create(config), logger) + end + end + + # Specify table columns which should be ignored by ActiveRecord, e.g.: + # + # ignore_table_columns :attribute1, :attribute2 + def self.ignore_table_columns(*args) + connection.ignore_table_columns(table_name,*args) + end + + # Specify which table columns should be typecasted to Date (without time), e.g.: + # + # set_date_columns :created_on, :updated_on + def self.set_date_columns(*args) + connection.set_type_for_columns(table_name,:date,*args) + end + + # Specify which table columns should be typecasted to Time (or DateTime), e.g.: + # + # set_datetime_columns :created_date, :updated_date + def self.set_datetime_columns(*args) + connection.set_type_for_columns(table_name,:datetime,*args) + end + + # Specify which table columns should be typecasted to boolean values +true+ or +false+, e.g.: + # + # set_boolean_columns :is_valid, :is_completed + def self.set_boolean_columns(*args) + connection.set_type_for_columns(table_name,:boolean,*args) + end + + # Specify which table columns should be typecasted to integer values. + # Might be useful to force NUMBER(1) column to be integer and not boolean, or force NUMBER column without + # scale to be retrieved as integer and not decimal. Example: + # + # set_integer_columns :version_number, :object_identifier + def self.set_integer_columns(*args) + connection.set_type_for_columns(table_name,:integer,*args) + end + + # Specify which table columns should be typecasted to string values. + # Might be useful to specify that columns should be string even if its name matches boolean column criteria. + # + # set_string_columns :active_flag + def self.set_string_columns(*args) + connection.set_type_for_columns(table_name,:string,*args) + end + + # After setting large objects to empty, select the OCI8::LOB + # and write back the data. + if ActiveRecord::VERSION::MAJOR == 3 && ActiveRecord::VERSION::MINOR >= 1 + after_update :enhanced_write_lobs + else + after_save :enhanced_write_lobs + end + def enhanced_write_lobs #:nodoc: + if connection.is_a?(ConnectionAdapters::OracleEnhancedAdapter) && + !(self.class.custom_create_method || self.class.custom_update_method) + connection.write_lobs(self.class.table_name, self.class, attributes) + end + end + private :enhanced_write_lobs + + # Get table comment from schema definition. + def self.table_comment + connection.table_comment(self.table_name) + end + + if ActiveRecord::VERSION::MAJOR < 3 + def attributes_with_quotes_with_virtual_columns(include_primary_key = true, include_readonly_attributes = true, attribute_names = @attributes.keys) + virtual_columns = self.class.columns.select(& :virtual?).map(&:name) + attributes_with_quotes_without_virtual_columns(include_primary_key, include_readonly_attributes, attribute_names - virtual_columns) + end + + alias_method_chain :attributes_with_quotes, :virtual_columns + else + def arel_attributes_values_with_virtual_columns(include_primary_key = true, include_readonly_attributes = true, attribute_names = @attributes.keys) + virtual_columns = self.class.columns.select(& :virtual?).map(&:name) + arel_attributes_values_without_virtual_columns(include_primary_key, include_readonly_attributes, attribute_names - virtual_columns) + end + + alias_method_chain :arel_attributes_values, :virtual_columns + end + + end + +end diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-oracle_enhanced-adapter-1.4.0/lib/active_record/connection_adapters/oracle_enhanced_column.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-oracle_enhanced-adapter-1.4.0/lib/active_record/connection_adapters/oracle_enhanced_column.rb new file mode 100644 index 00000000000..084713907bc --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-oracle_enhanced-adapter-1.4.0/lib/active_record/connection_adapters/oracle_enhanced_column.rb @@ -0,0 +1,136 @@ +module ActiveRecord + module ConnectionAdapters #:nodoc: + class OracleEnhancedColumn < Column + + attr_reader :table_name, :forced_column_type, :nchar, :virtual_column_data_default #:nodoc: + + def initialize(name, default, sql_type = nil, null = true, table_name = nil, forced_column_type = nil, virtual=false) #:nodoc: + @table_name = table_name + @forced_column_type = forced_column_type + @virtual = virtual + super(name, default, sql_type, null) + @virtual_column_data_default = default.inspect if virtual + # Is column NCHAR or NVARCHAR2 (will need to use N'...' value quoting for these data types)? + # Define only when needed as adapter "quote" method will check at first if instance variable is defined. + @nchar = true if @type == :string && sql_type[0,1] == 'N' + end + + def type_cast(value) #:nodoc: + return OracleEnhancedColumn::string_to_raw(value) if type == :raw + return guess_date_or_time(value) if type == :datetime && OracleEnhancedAdapter.emulate_dates + super + end + + def virtual? + @virtual + end + + # convert something to a boolean + # added y as boolean value + def self.value_to_boolean(value) #:nodoc: + if value == true || value == false + value + elsif value.is_a?(String) && value.blank? + nil + else + %w(true t 1 y +).include?(value.to_s.downcase) + end + end + + # convert Time or DateTime value to Date for :date columns + def self.string_to_date(string) #:nodoc: + return string.to_date if string.is_a?(Time) || string.is_a?(DateTime) + super + end + + # convert Date value to Time for :datetime columns + def self.string_to_time(string) #:nodoc: + return string.to_time if string.is_a?(Date) && !OracleEnhancedAdapter.emulate_dates + super + end + + # convert RAW column values back to byte strings. + def self.string_to_raw(string) #:nodoc: + string + end + + # Get column comment from schema definition. + # Will work only if using default ActiveRecord connection. + def comment + ActiveRecord::Base.connection.column_comment(@table_name, name) + end + + private + + def simplified_type(field_type) + forced_column_type || + case field_type + when /decimal|numeric|number/i + if OracleEnhancedAdapter.emulate_booleans && field_type == 'NUMBER(1)' + :boolean + elsif extract_scale(field_type) == 0 || + # if column name is ID or ends with _ID + OracleEnhancedAdapter.emulate_integers_by_column_name && OracleEnhancedAdapter.is_integer_column?(name, table_name) + :integer + else + :decimal + end + when /raw/i + :raw + when /char/i + if OracleEnhancedAdapter.emulate_booleans_from_strings && + OracleEnhancedAdapter.is_boolean_column?(name, field_type, table_name) + :boolean + else + :string + end + when /date/i + if OracleEnhancedAdapter.emulate_dates_by_column_name && OracleEnhancedAdapter.is_date_column?(name, table_name) + :date + else + :datetime + end + when /timestamp/i + :timestamp + when /time/i + :datetime + else + super + end + end + + def guess_date_or_time(value) + value.respond_to?(:hour) && (value.hour == 0 and value.min == 0 and value.sec == 0) ? + Date.new(value.year, value.month, value.day) : value + end + + class << self + protected + + def fallback_string_to_date(string) #:nodoc: + if OracleEnhancedAdapter.string_to_date_format || OracleEnhancedAdapter.string_to_time_format + return (string_to_date_or_time_using_format(string).to_date rescue super) + end + super + end + + def fallback_string_to_time(string) #:nodoc: + if OracleEnhancedAdapter.string_to_time_format || OracleEnhancedAdapter.string_to_date_format + return (string_to_date_or_time_using_format(string).to_time rescue super) + end + super + end + + def string_to_date_or_time_using_format(string) #:nodoc: + if OracleEnhancedAdapter.string_to_time_format && dt=Date._strptime(string, OracleEnhancedAdapter.string_to_time_format) + return Time.parse("#{dt[:year]}-#{dt[:mon]}-#{dt[:mday]} #{dt[:hour]}:#{dt[:min]}:#{dt[:sec]}#{dt[:zone]}") + end + DateTime.strptime(string, OracleEnhancedAdapter.string_to_date_format).to_date + end + + end + end + + end + +end diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-oracle_enhanced-adapter-1.4.0/lib/active_record/connection_adapters/oracle_enhanced_connection.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-oracle_enhanced-adapter-1.4.0/lib/active_record/connection_adapters/oracle_enhanced_connection.rb new file mode 100644 index 00000000000..22fe32dcc33 --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-oracle_enhanced-adapter-1.4.0/lib/active_record/connection_adapters/oracle_enhanced_connection.rb @@ -0,0 +1,119 @@ +module ActiveRecord + module ConnectionAdapters + # interface independent methods + class OracleEnhancedConnection #:nodoc: + + def self.create(config) + case ORACLE_ENHANCED_CONNECTION + when :oci + OracleEnhancedOCIConnection.new(config) + when :jdbc + OracleEnhancedJDBCConnection.new(config) + else + nil + end + end + + attr_reader :raw_connection + + # Oracle column names by default are case-insensitive, but treated as upcase; + # for neatness, we'll downcase within Rails. EXCEPT that folks CAN quote + # their column names when creating Oracle tables, which makes then case-sensitive. + # I don't know anybody who does this, but we'll handle the theoretical case of a + # camelCase column name. I imagine other dbs handle this different, since there's a + # unit test that's currently failing test_oci. + def oracle_downcase(column_name) + return nil if column_name.nil? + column_name =~ /[a-z]/ ? column_name : column_name.downcase + end + + # Used always by JDBC connection as well by OCI connection when describing tables over database link + def describe(name) + name = name.to_s + if name.include?('@') + name, db_link = name.split('@') + default_owner = select_value("SELECT username FROM all_db_links WHERE db_link = '#{db_link.upcase}'") + db_link = "@#{db_link}" + else + db_link = nil + default_owner = @owner + end + real_name = OracleEnhancedAdapter.valid_table_name?(name) ? name.upcase : name + if real_name.include?('.') + table_owner, table_name = real_name.split('.') + else + table_owner, table_name = default_owner, real_name + end + sql = <<-SQL + SELECT owner, table_name, 'TABLE' name_type + FROM all_tables#{db_link} + WHERE owner = '#{table_owner}' + AND table_name = '#{table_name}' + UNION ALL + SELECT owner, view_name table_name, 'VIEW' name_type + FROM all_views#{db_link} + WHERE owner = '#{table_owner}' + AND view_name = '#{table_name}' + UNION ALL + SELECT table_owner, DECODE(db_link, NULL, table_name, table_name||'@'||db_link), 'SYNONYM' name_type + FROM all_synonyms#{db_link} + WHERE owner = '#{table_owner}' + AND synonym_name = '#{table_name}' + UNION ALL + SELECT table_owner, DECODE(db_link, NULL, table_name, table_name||'@'||db_link), 'SYNONYM' name_type + FROM all_synonyms#{db_link} + WHERE owner = 'PUBLIC' + AND synonym_name = '#{real_name}' + SQL + if result = select_one(sql) + case result['name_type'] + when 'SYNONYM' + describe("#{result['owner'] && "#{result['owner']}."}#{result['table_name']}#{db_link}") + else + db_link ? [result['owner'], result['table_name'], db_link] : [result['owner'], result['table_name']] + end + else + raise OracleEnhancedConnectionException, %Q{"DESC #{name}" failed; does it exist?} + end + end + + # Returns a record hash with the column names as keys and column values + # as values. + def select_one(sql) + result = select(sql) + result.first if result + end + + # Returns a single value from a record + def select_value(sql) + if result = select_one(sql) + result.values.first + end + end + + # Returns an array of the values of the first column in a select: + # select_values("SELECT id FROM companies LIMIT 3") => [1,2,3] + def select_values(sql) + result = select(sql) + result.map { |r| r.values.first } + end + + end + + class OracleEnhancedConnectionException < StandardError #:nodoc: + end + + end +end + +# if MRI or YARV +if !defined?(RUBY_ENGINE) || RUBY_ENGINE == 'ruby' + ORACLE_ENHANCED_CONNECTION = :oci + require 'active_record/connection_adapters/oracle_enhanced_oci_connection' +# if JRuby +elsif RUBY_ENGINE == 'jruby' + ORACLE_ENHANCED_CONNECTION = :jdbc + require 'active_record/connection_adapters/oracle_enhanced_jdbc_connection' +else + raise "Unsupported Ruby engine #{RUBY_ENGINE}" +end diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-oracle_enhanced-adapter-1.4.0/lib/active_record/connection_adapters/oracle_enhanced_context_index.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-oracle_enhanced-adapter-1.4.0/lib/active_record/connection_adapters/oracle_enhanced_context_index.rb new file mode 100644 index 00000000000..ef296d22945 --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-oracle_enhanced-adapter-1.4.0/lib/active_record/connection_adapters/oracle_enhanced_context_index.rb @@ -0,0 +1,328 @@ +module ActiveRecord + module ConnectionAdapters + module OracleEnhancedContextIndex + + # Define full text index with Oracle specific CONTEXT index type + # + # Oracle CONTEXT index by default supports full text indexing of one column. + # This method allows full text index creation also on several columns + # as well as indexing related table columns by generating stored procedure + # that concatenates all columns for indexing as well as generating trigger + # that will update main index column to trigger reindexing of record. + # + # Use +contains+ ActiveRecord model instance method to add CONTAINS where condition + # and order by score of matched results. + # + # Options: + # + # * <tt>:name</tt> + # * <tt>:index_column</tt> + # * <tt>:index_column_trigger_on</tt> + # * <tt>:tablespace</tt> + # * <tt>:sync</tt> - 'MANUAL', 'EVERY "interval-string"' or 'ON COMMIT' (defaults to 'MANUAL'). + # * <tt>:lexer</tt> - Lexer options (e.g. <tt>:type => 'BASIC_LEXER', :base_letter => true</tt>). + # * <tt>:transactional</tt> - When +true+, the CONTAINS operator will process inserted and updated rows. + # + # ===== Examples + # + # ====== Creating single column index + # add_context_index :posts, :title + # search with + # Post.contains(:title, 'word') + # + # ====== Creating index on several columns + # add_context_index :posts, [:title, :body] + # search with (use first column as argument for contains method but it will search in all index columns) + # Post.contains(:title, 'word') + # + # ====== Creating index on several columns with dummy index column and commit option + # add_context_index :posts, [:title, :body], :index_column => :all_text, :sync => 'ON COMMIT' + # search with + # Post.contains(:all_text, 'word') + # + # ====== Creating index with trigger option (will reindex when specified columns are updated) + # add_context_index :posts, [:title, :body], :index_column => :all_text, :sync => 'ON COMMIT', + # :index_column_trigger_on => [:created_at, :updated_at] + # search with + # Post.contains(:all_text, 'word') + # + # ====== Creating index on multiple tables + # add_context_index :posts, + # [:title, :body, + # # specify aliases always with AS keyword + # "SELECT comments.author AS comment_author, comments.body AS comment_body FROM comments WHERE comments.post_id = :id" + # ], + # :name => 'post_and_comments_index', + # :index_column => :all_text, :index_column_trigger_on => [:updated_at, :comments_count], + # :sync => 'ON COMMIT' + # search in any table columns + # Post.contains(:all_text, 'word') + # search in specified column + # Post.contains(:all_text, "aaa within title") + # Post.contains(:all_text, "bbb within comment_author") + # + # ====== Creating index using lexer + # add_context_index :posts, :title, :lexer => { :type => 'BASIC_LEXER', :base_letter => true, ... } + # + # ====== Creating transactional index (will reindex changed rows when querying) + # add_context_index :posts, :title, :transactional => true + # + def add_context_index(table_name, column_name, options = {}) + self.all_schema_indexes = nil + column_names = Array(column_name) + index_name = options[:name] || index_name(table_name, :column => options[:index_column] || column_names, + # CONEXT index name max length is 25 + :identifier_max_length => 25) + + quoted_column_name = quote_column_name(options[:index_column] || column_names.first) + if options[:index_column_trigger_on] + raise ArgumentError, "Option :index_column should be specified together with :index_column_trigger_on option" \ + unless options[:index_column] + create_index_column_trigger(table_name, index_name, options[:index_column], options[:index_column_trigger_on]) + end + + sql = "CREATE INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)}" + sql << " (#{quoted_column_name})" + sql << " INDEXTYPE IS CTXSYS.CONTEXT" + parameters = [] + if column_names.size > 1 + procedure_name = default_datastore_procedure(index_name) + datastore_name = default_datastore_name(index_name) + create_datastore_procedure(table_name, procedure_name, column_names, options) + create_datastore_preference(datastore_name, procedure_name) + parameters << "DATASTORE #{datastore_name} SECTION GROUP CTXSYS.AUTO_SECTION_GROUP" + end + if options[:tablespace] + storage_name = default_storage_name(index_name) + create_storage_preference(storage_name, options[:tablespace]) + parameters << "STORAGE #{storage_name}" + end + if options[:sync] + parameters << "SYNC(#{options[:sync]})" + end + if options[:lexer] && (lexer_type = options[:lexer][:type]) + lexer_name = default_lexer_name(index_name) + (lexer_options = options[:lexer].dup).delete(:type) + create_lexer_preference(lexer_name, lexer_type, lexer_options) + parameters << "LEXER #{lexer_name}" + end + if options[:transactional] + parameters << "TRANSACTIONAL" + end + unless parameters.empty? + sql << " PARAMETERS ('#{parameters.join(' ')}')" + end + execute sql + end + + # Drop full text index with Oracle specific CONTEXT index type + def remove_context_index(table_name, options = {}) + self.all_schema_indexes = nil + unless Hash === options # if column names passed as argument + options = {:column => Array(options)} + end + index_name = options[:name] || index_name(table_name, + :column => options[:index_column] || options[:column], :identifier_max_length => 25) + execute "DROP INDEX #{index_name}" + drop_ctx_preference(default_datastore_name(index_name)) + drop_ctx_preference(default_storage_name(index_name)) + procedure_name = default_datastore_procedure(index_name) + execute "DROP PROCEDURE #{quote_table_name(procedure_name)}" rescue nil + drop_index_column_trigger(index_name) + end + + private + + def create_datastore_procedure(table_name, procedure_name, column_names, options) + quoted_table_name = quote_table_name(table_name) + select_queries, column_names = column_names.partition { |c| c.to_s =~ /^\s*SELECT\s+/i } + select_queries = select_queries.map { |s| s.strip.gsub(/\s+/, ' ') } + keys, selected_columns = parse_select_queries(select_queries) + quoted_column_names = (column_names+keys).map{|col| quote_column_name(col)} + execute compress_lines(<<-SQL) + CREATE OR REPLACE PROCEDURE #{quote_table_name(procedure_name)} + (p_rowid IN ROWID, + p_clob IN OUT NOCOPY CLOB) IS + -- add_context_index_parameters #{(column_names+select_queries).inspect}#{!options.empty? ? ', ' << options.inspect[1..-2] : ''} + #{ + selected_columns.map do |cols| + cols.map do |col| + raise ArgumentError, "Alias #{col} too large, should be 28 or less characters long" unless col.length <= 28 + "l_#{col} VARCHAR2(32767);\n" + end.join + end.join + } BEGIN + FOR r1 IN ( + SELECT #{quoted_column_names.join(', ')} + FROM #{quoted_table_name} + WHERE #{quoted_table_name}.ROWID = p_rowid + ) LOOP + #{ + (column_names.map do |col| + col = col.to_s + "DBMS_LOB.WRITEAPPEND(p_clob, #{col.length+2}, '<#{col}>');\n" << + "IF LENGTH(r1.#{col}) > 0 THEN\n" << + "DBMS_LOB.WRITEAPPEND(p_clob, LENGTH(r1.#{col}), r1.#{col});\n" << + "END IF;\n" << + "DBMS_LOB.WRITEAPPEND(p_clob, #{col.length+3}, '</#{col}>');\n" + end.join) << + (selected_columns.zip(select_queries).map do |cols, query| + (cols.map do |col| + "l_#{col} := '';\n" + end.join) << + "FOR r2 IN (\n" << + query.gsub(/:(\w+)/,"r1.\\1") << "\n) LOOP\n" << + (cols.map do |col| + "l_#{col} := l_#{col} || r2.#{col} || CHR(10);\n" + end.join) << + "END LOOP;\n" << + (cols.map do |col| + col = col.to_s + "DBMS_LOB.WRITEAPPEND(p_clob, #{col.length+2}, '<#{col}>');\n" << + "IF LENGTH(l_#{col}) > 0 THEN\n" << + "DBMS_LOB.WRITEAPPEND(p_clob, LENGTH(l_#{col}), l_#{col});\n" << + "END IF;\n" << + "DBMS_LOB.WRITEAPPEND(p_clob, #{col.length+3}, '</#{col}>');\n" + end.join) + end.join) + } + END LOOP; + END; + SQL + end + + def parse_select_queries(select_queries) + keys = [] + selected_columns = [] + select_queries.each do |query| + # get primary or foreign keys like :id or :something_id + keys << (query.scan(/:\w+/).map{|k| k[1..-1].downcase.to_sym}) + select_part = query.scan(/^select\s.*\sfrom/i).first + selected_columns << select_part.scan(/\sas\s+(\w+)/i).map{|c| c.first} + end + [keys.flatten.uniq, selected_columns] + end + + def create_datastore_preference(datastore_name, procedure_name) + drop_ctx_preference(datastore_name) + execute <<-SQL + BEGIN + CTX_DDL.CREATE_PREFERENCE('#{datastore_name}', 'USER_DATASTORE'); + CTX_DDL.SET_ATTRIBUTE('#{datastore_name}', 'PROCEDURE', '#{procedure_name}'); + END; + SQL + end + + def create_storage_preference(storage_name, tablespace) + drop_ctx_preference(storage_name) + sql = "BEGIN\nCTX_DDL.CREATE_PREFERENCE('#{storage_name}', 'BASIC_STORAGE');\n" + ['I_TABLE_CLAUSE', 'K_TABLE_CLAUSE', 'R_TABLE_CLAUSE', + 'N_TABLE_CLAUSE', 'I_INDEX_CLAUSE', 'P_TABLE_CLAUSE'].each do |clause| + default_clause = case clause + when 'R_TABLE_CLAUSE'; 'LOB(DATA) STORE AS (CACHE) ' + when 'I_INDEX_CLAUSE'; 'COMPRESS 2 ' + else '' + end + sql << "CTX_DDL.SET_ATTRIBUTE('#{storage_name}', '#{clause}', '#{default_clause}TABLESPACE #{tablespace}');\n" + end + sql << "END;\n" + execute sql + end + + def create_lexer_preference(lexer_name, lexer_type, options) + drop_ctx_preference(lexer_name) + sql = "BEGIN\nCTX_DDL.CREATE_PREFERENCE('#{lexer_name}', '#{lexer_type}');\n" + options.each do |key, value| + plsql_value = case value + when String; "'#{value}'" + when true; "'YES'" + when false; "'NO'" + when nil; 'NULL' + else value + end + sql << "CTX_DDL.SET_ATTRIBUTE('#{lexer_name}', '#{key}', #{plsql_value});\n" + end + sql << "END;\n" + execute sql + end + + def drop_ctx_preference(preference_name) + execute "BEGIN CTX_DDL.DROP_PREFERENCE('#{preference_name}'); END;" rescue nil + end + + def create_index_column_trigger(table_name, index_name, index_column, index_column_source) + trigger_name = default_index_column_trigger_name(index_name) + columns = Array(index_column_source) + quoted_column_names = columns.map{|col| quote_column_name(col)}.join(', ') + execute compress_lines(<<-SQL) + CREATE OR REPLACE TRIGGER #{quote_table_name(trigger_name)} + BEFORE UPDATE OF #{quoted_column_names} ON #{quote_table_name(table_name)} FOR EACH ROW + BEGIN + :new.#{quote_column_name(index_column)} := '1'; + END; + SQL + end + + def drop_index_column_trigger(index_name) + trigger_name = default_index_column_trigger_name(index_name) + execute "DROP TRIGGER #{quote_table_name(trigger_name)}" rescue nil + end + + def default_datastore_procedure(index_name) + "#{index_name}_prc" + end + + def default_datastore_name(index_name) + "#{index_name}_dst" + end + + def default_storage_name(index_name) + "#{index_name}_sto" + end + + def default_index_column_trigger_name(index_name) + "#{index_name}_trg" + end + + def default_lexer_name(index_name) + "#{index_name}_lex" + end + + module BaseClassMethods + # Declare that model table has context index defined. + # As a result <tt>contains</tt> class scope method is defined. + def has_context_index + extend ContextIndexClassMethods + end + end + + module ContextIndexClassMethods + # Add context index condition. + case ::ActiveRecord::VERSION::MAJOR + when 3 + def contains(column, query, options ={}) + score_label = options[:label].to_i || 1 + where("CONTAINS(#{connection.quote_column_name(column)}, ?, #{score_label}) > 0", query). + order("SCORE(#{score_label}) DESC") + end + when 2 + def contains(column, query, options ={}) + score_label = options[:label].to_i || 1 + scoped(:conditions => ["CONTAINS(#{connection.quote_column_name(column)}, ?, #{score_label}) > 0", query], + :order => "SCORE(#{score_label}) DESC") + end + end + end + + end + + end +end + +ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.class_eval do + include ActiveRecord::ConnectionAdapters::OracleEnhancedContextIndex +end + +ActiveRecord::Base.class_eval do + extend ActiveRecord::ConnectionAdapters::OracleEnhancedContextIndex::BaseClassMethods +end diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-oracle_enhanced-adapter-1.4.0/lib/active_record/connection_adapters/oracle_enhanced_core_ext.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-oracle_enhanced-adapter-1.4.0/lib/active_record/connection_adapters/oracle_enhanced_core_ext.rb new file mode 100644 index 00000000000..0570c0e4b99 --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-oracle_enhanced-adapter-1.4.0/lib/active_record/connection_adapters/oracle_enhanced_core_ext.rb @@ -0,0 +1,24 @@ +require "bigdecimal" +if (BigDecimal.instance_methods & ["to_d", :to_d]).empty? + BigDecimal.class_eval do + def to_d #:nodoc: + self + end + end +end + +if (Bignum.instance_methods & ["to_d", :to_d]).empty? + Bignum.class_eval do + def to_d #:nodoc: + BigDecimal.new(self.to_s) + end + end +end + +if (Fixnum.instance_methods & ["to_d", :to_d]).empty? + Fixnum.class_eval do + def to_d #:nodoc: + BigDecimal.new(self.to_s) + end + end +end diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-oracle_enhanced-adapter-1.4.0/lib/active_record/connection_adapters/oracle_enhanced_cpk.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-oracle_enhanced-adapter-1.4.0/lib/active_record/connection_adapters/oracle_enhanced_cpk.rb new file mode 100644 index 00000000000..05cae114e84 --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-oracle_enhanced-adapter-1.4.0/lib/active_record/connection_adapters/oracle_enhanced_cpk.rb @@ -0,0 +1,21 @@ +module ActiveRecord #:nodoc: + module ConnectionAdapters #:nodoc: + module OracleEnhancedCpk #:nodoc: + + # This mightn't be in Core, but count(distinct x,y) doesn't work for me. + # Return that not supported if composite_primary_keys gem is required. + def supports_count_distinct? #:nodoc: + @supports_count_distinct ||= ! defined?(CompositePrimaryKeys) + end + + def concat(*columns) #:nodoc: + "(#{columns.join('||')})" + end + + end + end +end + +ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.class_eval do + include ActiveRecord::ConnectionAdapters::OracleEnhancedCpk +end diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-oracle_enhanced-adapter-1.4.0/lib/active_record/connection_adapters/oracle_enhanced_dirty.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-oracle_enhanced-adapter-1.4.0/lib/active_record/connection_adapters/oracle_enhanced_dirty.rb new file mode 100644 index 00000000000..9c9df304795 --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-oracle_enhanced-adapter-1.4.0/lib/active_record/connection_adapters/oracle_enhanced_dirty.rb @@ -0,0 +1,39 @@ +module ActiveRecord #:nodoc: + module ConnectionAdapters #:nodoc: + module OracleEnhancedDirty #:nodoc: + + module InstanceMethods #:nodoc: + private + + def field_changed?(attr, old, value) + if column = column_for_attribute(attr) + # Added also :decimal type + if (column.type == :integer || column.type == :decimal) && column.null && (old.nil? || old == 0) && value.blank? + # For nullable integer columns, NULL gets stored in database for blank (i.e. '') values. + # Hence we don't record it as a change if the value changes from nil to ''. + # If an old value of 0 is set to '' we want this to get changed to nil as otherwise it'll + # be typecast back to 0 (''.to_i => 0) + value = nil + # Oracle stores empty string '' as NULL + # therefore need to convert empty string value to nil if old value is nil + elsif column.type == :string && column.null && old.nil? + value = nil if value == '' + else + value = column.type_cast(value) + end + end + + old != value + end + + end + + end + end +end + +if ActiveRecord::Base.method_defined?(:changed?) + ActiveRecord::Base.class_eval do + include ActiveRecord::ConnectionAdapters::OracleEnhancedDirty::InstanceMethods + end +end
\ No newline at end of file diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-oracle_enhanced-adapter-1.4.0/lib/active_record/connection_adapters/oracle_enhanced_jdbc_connection.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-oracle_enhanced-adapter-1.4.0/lib/active_record/connection_adapters/oracle_enhanced_jdbc_connection.rb new file mode 100644 index 00000000000..6a6694c1e2d --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-oracle_enhanced-adapter-1.4.0/lib/active_record/connection_adapters/oracle_enhanced_jdbc_connection.rb @@ -0,0 +1,484 @@ +#begin +# require "java" +# require "jruby" +# +# # ojdbc6.jar or ojdbc5.jar file should be in JRUBY_HOME/lib or should be in ENV['PATH'] or load path +# +# java_version = java.lang.System.getProperty("java.version") +# ojdbc_jar = if java_version =~ /^1.5/ +# "ojdbc5.jar" +# elsif java_version >= '1.6' +# "ojdbc6.jar" +# else +# nil +# end +# +# unless ojdbc_jar.nil? || ENV_JAVA['java.class.path'] =~ Regexp.new(ojdbc_jar) +# # On Unix environment variable should be PATH, on Windows it is sometimes Path +# env_path = (ENV["PATH"] || ENV["Path"] || '').split(/[:;]/) +# # Look for JDBC driver at first in lib subdirectory (application specific JDBC file version) +# # then in Ruby load path and finally in environment PATH +# if ojdbc_jar_path = ['./lib'].concat($LOAD_PATH).concat(env_path).find{|d| File.exists?(File.join(d,ojdbc_jar))} +# require File.join(ojdbc_jar_path,ojdbc_jar) +# end +# end +# +# java.sql.DriverManager.registerDriver Java::oracle.jdbc.OracleDriver.new +# +# # set tns_admin property from TNS_ADMIN environment variable +# if !java.lang.System.get_property("oracle.net.tns_admin") && ENV["TNS_ADMIN"] +# java.lang.System.set_property("oracle.net.tns_admin", ENV["TNS_ADMIN"]) +# end +# +#rescue LoadError, NameError +# # JDBC driver is unavailable. +# raise LoadError, "ERROR: ActiveRecord oracle_enhanced adapter could not load Oracle JDBC driver. Please install #{ojdbc_jar || "Oracle JDBC"} library." +#end + + +module ActiveRecord + module ConnectionAdapters + + # JDBC database interface for JRuby + class OracleEnhancedJDBCConnection < OracleEnhancedConnection #:nodoc: + + attr_accessor :active + alias :active? :active + + attr_accessor :auto_retry + alias :auto_retry? :auto_retry + @auto_retry = false + + def initialize(config) + @active = true + @config = config + new_connection(@config) + end + + #sonar + # modified method to support JNDI connections + def new_connection(config) + @raw_connection = ::Java::OrgSonarServerUi::JRubyFacade.getInstance().getConnection() + if @raw_connection.respond_to?(:getInnermostDelegate) + @pooled_connection = @raw_connection + @raw_connection = @raw_connection.innermost_delegate + elsif @raw_connection.respond_to?(:getUnderlyingConnection) + @pooled_connection = @raw_connection + @raw_connection = @raw_connection.underlying_connection + else + raise ArgumentError, "JDBC Datasource not supported. Please use Commons DBCP or Tomcat Connection Pool" + end + username = @raw_connection.meta_data.user_name + + self.autocommit = true + + # default schema owner + @owner = username.upcase unless username.nil? + + @raw_connection + end + #/sonar + + def logoff + @active = false + if defined?(@pooled_connection) + @pooled_connection.close + else + @raw_connection.close + end + true + rescue + false + end + + def commit + @raw_connection.commit + end + + def rollback + @raw_connection.rollback + end + + def autocommit? + @raw_connection.getAutoCommit + end + + def autocommit=(value) + @raw_connection.setAutoCommit(value) + end + + # Checks connection, returns true if active. Note that ping actively + # checks the connection, while #active? simply returns the last + # known state. + def ping + exec_no_retry("select 1 from dual") + @active = true + rescue NativeException => e + @active = false + if e.message =~ /^java\.sql\.SQL(Recoverable)?Exception/ + raise OracleEnhancedConnectionException, e.message + else + raise + end + end + + # Resets connection, by logging off and creating a new connection. + def reset! + logoff rescue nil + begin + new_connection(@config) + @active = true + rescue NativeException => e + @active = false + if e.message =~ /^java\.sql\.SQL(Recoverable)?Exception/ + raise OracleEnhancedConnectionException, e.message + else + raise + end + end + end + + # mark connection as dead if connection lost + def with_retry(&block) + should_retry = auto_retry? && autocommit? + begin + yield if block_given? + rescue NativeException => e + raise unless e.message =~ /^java\.sql\.SQL(Recoverable)?Exception: (Closed Connection|Io exception:|No more data to read from socket|IO Error:)/ + @active = false + raise unless should_retry + should_retry = false + reset! rescue nil + retry + end + end + + def exec(sql) + with_retry do + exec_no_retry(sql) + end + end + + def exec_no_retry(sql) + case sql + when /\A\s*(UPDATE|INSERT|DELETE)/i + s = @raw_connection.prepareStatement(sql) + s.executeUpdate + # it is safer for CREATE and DROP statements not to use PreparedStatement + # as it does not allow creation of triggers with :NEW in their definition + when /\A\s*(CREATE|DROP)/i + s = @raw_connection.createStatement() + # this disables SQL92 syntax processing of {...} which can result in statement execution errors + # if sql contains {...} in strings or comments + s.setEscapeProcessing(false) + s.execute(sql) + true + else + s = @raw_connection.prepareStatement(sql) + s.execute + true + end + ensure + s.close rescue nil + end + + def returning_clause(quoted_pk) + " RETURNING #{quoted_pk} INTO ?" + end + + # execute sql with RETURNING ... INTO :insert_id + # and return :insert_id value + def exec_with_returning(sql) + with_retry do + begin + # it will always be INSERT statement + + # TODO: need to investigate why PreparedStatement is giving strange exception "Protocol violation" + # s = @raw_connection.prepareStatement(sql) + # s.registerReturnParameter(1, ::Java::oracle.jdbc.OracleTypes::NUMBER) + # count = s.executeUpdate + # if count > 0 + # rs = s.getReturnResultSet + # if rs.next + # # Assuming that primary key will not be larger as long max value + # insert_id = rs.getLong(1) + # rs.wasNull ? nil : insert_id + # else + # nil + # end + # else + # nil + # end + + # Workaround with CallableStatement + s = @raw_connection.prepareCall("BEGIN #{sql}; END;") + s.registerOutParameter(1, java.sql.Types::BIGINT) + s.execute + insert_id = s.getLong(1) + s.wasNull ? nil : insert_id + ensure + # rs.close rescue nil + s.close rescue nil + end + end + end + + def prepare(sql) + Cursor.new(self, @raw_connection.prepareStatement(sql)) + end + + class Cursor + def initialize(connection, raw_statement) + @connection = connection + @raw_statement = raw_statement + end + + def bind_param(position, value, col_type = nil) + java_value = ruby_to_java_value(value, col_type) + case value + when Integer + @raw_statement.setLong(position, java_value) + when Float + @raw_statement.setFloat(position, java_value) + when BigDecimal + @raw_statement.setBigDecimal(position, java_value) + when String + case col_type + when :text + @raw_statement.setClob(position, java_value) + when :binary + @raw_statement.setBlob(position, java_value) + when :raw + @raw_statement.setString(position, OracleEnhancedAdapter.encode_raw(java_value)) + else + @raw_statement.setString(position, java_value) + end + when Date, DateTime + @raw_statement.setDATE(position, java_value) + when Time + @raw_statement.setTimestamp(position, java_value) + when NilClass + # TODO: currently nil is always bound as NULL with VARCHAR type. + # When nils will actually be used by ActiveRecord as bound parameters + # then need to pass actual column type. + @raw_statement.setNull(position, java.sql.Types::VARCHAR) + else + raise ArgumentError, "Don't know how to bind variable with type #{value.class}" + end + end + + def bind_returning_param(position, bind_type) + @returning_positions ||= [] + @returning_positions << position + if bind_type == Integer + @raw_statement.registerReturnParameter(position, java.sql.Types::BIGINT) + end + end + + def exec + @raw_result_set = @raw_statement.executeQuery + get_metadata + true + end + + def exec_update + @raw_statement.executeUpdate + end + + def get_metadata + metadata = @raw_result_set.getMetaData + column_count = metadata.getColumnCount + @column_names = (1..column_count).map { |i| metadata.getColumnName(i) } + @column_types = (1..column_count).map { |i| metadata.getColumnTypeName(i).to_sym } + end + + def get_col_names + @column_names + end + + def fetch(options={}) + if @raw_result_set.next + get_lob_value = options[:get_lob_value] + row_values = [] + @column_types.each_with_index do |column_type, i| + row_values << + @connection.get_ruby_value_from_result_set(@raw_result_set, i+1, column_type, get_lob_value) + end + row_values + else + @raw_result_set.close + nil + end + end + + def get_returning_param(position, type) + rs_position = @returning_positions.index(position) + 1 + rs = @raw_statement.getReturnResultSet + if rs.next + # Assuming that primary key will not be larger as long max value + returning_id = rs.getLong(rs_position) + rs.wasNull ? nil : returning_id + else + nil + end + end + + def close + @raw_statement.close + end + + private + + def ruby_to_java_value(value, col_type = nil) + case value + when Fixnum, Float + value + when String + case col_type + when :text + clob = Java::OracleSql::CLOB.createTemporary(@connection.raw_connection, false, Java::OracleSql::CLOB::DURATION_SESSION) + clob.setString(1, value) + clob + when :binary + blob = Java::OracleSql::BLOB.createTemporary(@connection.raw_connection, false, Java::OracleSql::BLOB::DURATION_SESSION) + blob.setBytes(1, value.to_java_bytes) + blob + else + value + end + when BigDecimal + java.math.BigDecimal.new(value.to_s) + when Date, DateTime + Java::oracle.sql.DATE.new(value.strftime("%Y-%m-%d %H:%M:%S")) + when Time + Java::java.sql.Timestamp.new(value.year-1900, value.month-1, value.day, value.hour, value.min, value.sec, value.usec * 1000) + else + value + end + end + + end + + def select(sql, name = nil, return_column_names = false) + with_retry do + select_no_retry(sql, name, return_column_names) + end + end + + def select_no_retry(sql, name = nil, return_column_names = false) + stmt = @raw_connection.prepareStatement(sql) + rset = stmt.executeQuery + + # Reuse the same hash for all rows + column_hash = {} + + metadata = rset.getMetaData + column_count = metadata.getColumnCount + + cols_types_index = (1..column_count).map do |i| + col_name = oracle_downcase(metadata.getColumnName(i)) + next if col_name == 'raw_rnum_' + column_hash[col_name] = nil + [col_name, metadata.getColumnTypeName(i).to_sym, i] + end + cols_types_index.delete(nil) + + rows = [] + get_lob_value = !(name == 'Writable Large Object') + + while rset.next + hash = column_hash.dup + cols_types_index.each do |col, column_type, i| + hash[col] = get_ruby_value_from_result_set(rset, i, column_type, get_lob_value) + end + rows << hash + end + + return_column_names ? [rows, cols_types_index.map(&:first)] : rows + ensure + rset.close rescue nil + stmt.close rescue nil + end + + def write_lob(lob, value, is_binary = false) + if is_binary + lob.setBytes(1, value.to_java_bytes) + else + lob.setString(1, value) + end + end + + # Return NativeException / java.sql.SQLException error code + def error_code(exception) + case exception + when NativeException + exception.cause.getErrorCode + else + nil + end + end + + def get_ruby_value_from_result_set(rset, i, type_name, get_lob_value = true) + case type_name + when :NUMBER + d = rset.getNUMBER(i) + if d.nil? + nil + elsif d.isInt + Integer(d.stringValue) + else + BigDecimal.new(d.stringValue) + end + when :VARCHAR2, :CHAR, :LONG, :NVARCHAR2, :NCHAR + rset.getString(i) + when :DATE + if dt = rset.getDATE(i) + d = dt.dateValue + t = dt.timeValue + if OracleEnhancedAdapter.emulate_dates && t.hours == 0 && t.minutes == 0 && t.seconds == 0 + Date.new(d.year + 1900, d.month + 1, d.date) + else + Time.send(Base.default_timezone, d.year + 1900, d.month + 1, d.date, t.hours, t.minutes, t.seconds) + end + else + nil + end + when :TIMESTAMP, :TIMESTAMPTZ, :TIMESTAMPLTZ, :"TIMESTAMP WITH TIME ZONE", :"TIMESTAMP WITH LOCAL TIME ZONE" + ts = rset.getTimestamp(i) + ts && Time.send(Base.default_timezone, ts.year + 1900, ts.month + 1, ts.date, ts.hours, ts.minutes, ts.seconds, + ts.nanos / 1000) + when :CLOB + get_lob_value ? lob_to_ruby_value(rset.getClob(i)) : rset.getClob(i) + when :BLOB + get_lob_value ? lob_to_ruby_value(rset.getBlob(i)) : rset.getBlob(i) + when :RAW + raw_value = rset.getRAW(i) + raw_value && raw_value.getBytes.to_a.pack('C*') + else + nil + end + end + + private + + def lob_to_ruby_value(val) + case val + when ::Java::OracleSql::CLOB + if val.isEmptyLob + nil + else + val.getSubString(1, val.length) + end + when ::Java::OracleSql::BLOB + if val.isEmptyLob + nil + else + String.from_java_bytes(val.getBytes(1, val.length)) + end + end + end + + end + + end +end diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-oracle_enhanced-adapter-1.4.0/lib/active_record/connection_adapters/oracle_enhanced_oci_connection.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-oracle_enhanced-adapter-1.4.0/lib/active_record/connection_adapters/oracle_enhanced_oci_connection.rb new file mode 100644 index 00000000000..8f56d7014c6 --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-oracle_enhanced-adapter-1.4.0/lib/active_record/connection_adapters/oracle_enhanced_oci_connection.rb @@ -0,0 +1,488 @@ +require 'delegate' + +begin + require "oci8" +rescue LoadError + # OCI8 driver is unavailable. + raise LoadError, "ERROR: ActiveRecord oracle_enhanced adapter could not load ruby-oci8 library. Please install ruby-oci8 gem." +end + +# check ruby-oci8 version +required_oci8_version = [2, 0, 3] +oci8_version_ints = OCI8::VERSION.scan(/\d+/).map{|s| s.to_i} +if (oci8_version_ints <=> required_oci8_version) < 0 + raise LoadError, "ERROR: ruby-oci8 version #{OCI8::VERSION} is too old. Please install ruby-oci8 version #{required_oci8_version.join('.')} or later." +end + +module ActiveRecord + module ConnectionAdapters + + # OCI database interface for MRI + class OracleEnhancedOCIConnection < OracleEnhancedConnection #:nodoc: + + def initialize(config) + @raw_connection = OCI8EnhancedAutoRecover.new(config, OracleEnhancedOCIFactory) + # default schema owner + @owner = config[:username].to_s.upcase + end + + def raw_oci_connection + if @raw_connection.is_a? OCI8 + @raw_connection + # ActiveRecord Oracle enhanced adapter puts OCI8EnhancedAutoRecover wrapper around OCI8 + # in this case we need to pass original OCI8 connection + else + @raw_connection.instance_variable_get(:@connection) + end + end + + def auto_retry + @raw_connection.auto_retry if @raw_connection + end + + def auto_retry=(value) + @raw_connection.auto_retry = value if @raw_connection + end + + def logoff + @raw_connection.logoff + @raw_connection.active = false + end + + def commit + @raw_connection.commit + end + + def rollback + @raw_connection.rollback + end + + def autocommit? + @raw_connection.autocommit? + end + + def autocommit=(value) + @raw_connection.autocommit = value + end + + # Checks connection, returns true if active. Note that ping actively + # checks the connection, while #active? simply returns the last + # known state. + def ping + @raw_connection.ping + rescue OCIException => e + raise OracleEnhancedConnectionException, e.message + end + + def active? + @raw_connection.active? + end + + def reset! + @raw_connection.reset! + rescue OCIException => e + raise OracleEnhancedConnectionException, e.message + end + + def exec(sql, *bindvars, &block) + @raw_connection.exec(sql, *bindvars, &block) + end + + def returning_clause(quoted_pk) + " RETURNING #{quoted_pk} INTO :insert_id" + end + + # execute sql with RETURNING ... INTO :insert_id + # and return :insert_id value + def exec_with_returning(sql) + cursor = @raw_connection.parse(sql) + cursor.bind_param(':insert_id', nil, Integer) + cursor.exec + cursor[':insert_id'] + ensure + cursor.close rescue nil + end + + def prepare(sql) + Cursor.new(self, @raw_connection.parse(sql)) + end + + class Cursor + def initialize(connection, raw_cursor) + @connection = connection + @raw_cursor = raw_cursor + end + + def bind_param(position, value, col_type = nil) + if value.nil? + @raw_cursor.bind_param(position, nil, String) + else + case col_type + when :text, :binary + # ruby-oci8 cannot create CLOB/BLOB from '' + lob_value = value == '' ? ' ' : value + bind_type = col_type == :text ? OCI8::CLOB : OCI8::BLOB + ora_value = bind_type.new(@connection.raw_oci_connection, lob_value) + ora_value.size = 0 if value == '' + @raw_cursor.bind_param(position, ora_value) + when :raw + @raw_cursor.bind_param(position, OracleEnhancedAdapter.encode_raw(value)) + else + @raw_cursor.bind_param(position, value) + end + end + end + + def bind_returning_param(position, bind_type) + @raw_cursor.bind_param(position, nil, bind_type) + end + + def exec + @raw_cursor.exec + end + + def exec_update + @raw_cursor.exec + end + + def get_col_names + @raw_cursor.get_col_names + end + + def fetch(options={}) + if row = @raw_cursor.fetch + get_lob_value = options[:get_lob_value] + row.map do |col| + @connection.typecast_result_value(col, get_lob_value) + end + end + end + + def get_returning_param(position, type) + @raw_cursor[position] + end + + def close + @raw_cursor.close + end + + end + + def select(sql, name = nil, return_column_names = false) + cursor = @raw_connection.exec(sql) + cols = [] + # Ignore raw_rnum_ which is used to simulate LIMIT and OFFSET + cursor.get_col_names.each do |col_name| + col_name = oracle_downcase(col_name) + cols << col_name unless col_name == 'raw_rnum_' + end + # Reuse the same hash for all rows + column_hash = {} + cols.each {|c| column_hash[c] = nil} + rows = [] + get_lob_value = !(name == 'Writable Large Object') + + while row = cursor.fetch + hash = column_hash.dup + + cols.each_with_index do |col, i| + hash[col] = typecast_result_value(row[i], get_lob_value) + end + + rows << hash + end + + return_column_names ? [rows, cols] : rows + ensure + cursor.close if cursor + end + + def write_lob(lob, value, is_binary = false) + lob.write value + end + + def describe(name) + # fall back to SELECT based describe if using database link + return super if name.to_s.include?('@') + quoted_name = OracleEnhancedAdapter.valid_table_name?(name) ? name : "\"#{name}\"" + @raw_connection.describe(quoted_name) + rescue OCIException => e + # fall back to SELECT which can handle synonyms to database links + super + end + + # Return OCIError error code + def error_code(exception) + case exception + when OCIError + exception.code + else + nil + end + end + + def typecast_result_value(value, get_lob_value) + case value + when Fixnum, Bignum + value + when String + value + when Float, BigDecimal + # return Fixnum or Bignum if value is integer (to avoid issues with _before_type_cast values for id attributes) + value == (v_to_i = value.to_i) ? v_to_i : value + when OraNumber + # change OraNumber value (returned in early versions of ruby-oci8 2.0.x) to BigDecimal + value == (v_to_i = value.to_i) ? v_to_i : BigDecimal.new(value.to_s) + when OCI8::LOB + if get_lob_value + data = value.read || "" # if value.read returns nil, then we have an empty_clob() i.e. an empty string + # In Ruby 1.9.1 always change encoding to ASCII-8BIT for binaries + data.force_encoding('ASCII-8BIT') if data.respond_to?(:force_encoding) && value.is_a?(OCI8::BLOB) + data + else + value + end + # ruby-oci8 1.0 returns OraDate + # ruby-oci8 2.0 returns Time or DateTime + when OraDate, Time, DateTime + if OracleEnhancedAdapter.emulate_dates && date_without_time?(value) + value.to_date + else + create_time_with_default_timezone(value) + end + else + value + end + end + + private + + def date_without_time?(value) + case value + when OraDate + value.hour == 0 && value.minute == 0 && value.second == 0 + else + value.hour == 0 && value.min == 0 && value.sec == 0 + end + end + + def create_time_with_default_timezone(value) + year, month, day, hour, min, sec, usec = case value + when Time + [value.year, value.month, value.day, value.hour, value.min, value.sec, value.usec] + when OraDate + [value.year, value.month, value.day, value.hour, value.minute, value.second, 0] + else + [value.year, value.month, value.day, value.hour, value.min, value.sec, 0] + end + # code from Time.time_with_datetime_fallback + begin + Time.send(Base.default_timezone, year, month, day, hour, min, sec, usec) + rescue + offset = Base.default_timezone.to_sym == :local ? ::DateTime.local_offset : 0 + ::DateTime.civil(year, month, day, hour, min, sec, offset) + end + end + + end + + # The OracleEnhancedOCIFactory factors out the code necessary to connect and + # configure an Oracle/OCI connection. + class OracleEnhancedOCIFactory #:nodoc: + def self.new_connection(config) + # to_s needed if username, password or database is specified as number in database.yml file + username = config[:username] && config[:username].to_s + password = config[:password] && config[:password].to_s + database = config[:database] && config[:database].to_s + host, port = config[:host], config[:port] + privilege = config[:privilege] && config[:privilege].to_sym + async = config[:allow_concurrency] + prefetch_rows = config[:prefetch_rows] || 100 + cursor_sharing = config[:cursor_sharing] || 'force' + # get session time_zone from configuration or from TZ environment variable + time_zone = config[:time_zone] || ENV['TZ'] + + # connection using host, port and database name + connection_string = if host || port + host ||= 'localhost' + host = "[#{host}]" if host =~ /^[^\[].*:/ # IPv6 + port ||= 1521 + "//#{host}:#{port}/#{database}" + # if no host is specified then assume that + # database parameter is TNS alias or TNS connection string + else + database + end + + conn = OCI8.new username, password, connection_string, privilege + conn.autocommit = true + conn.non_blocking = true if async + conn.prefetch_rows = prefetch_rows + conn.exec "alter session set cursor_sharing = #{cursor_sharing}" rescue nil + conn.exec "alter session set time_zone = '#{time_zone}'" unless time_zone.blank? + + # Initialize NLS parameters + OracleEnhancedAdapter::DEFAULT_NLS_PARAMETERS.each do |key, default_value| + value = config[key] || ENV[key.to_s.upcase] || default_value + if value + conn.exec "alter session set #{key} = '#{value}'" + end + end + conn + end + end + + + end +end + + + +class OCI8 #:nodoc: + + class Cursor #:nodoc: + if method_defined? :define_a_column + # This OCI8 patch is required with the ruby-oci8 1.0.x or lower. + # Set OCI8::BindType::Mapping[] to change the column type + # when using ruby-oci8 2.0. + + alias :enhanced_define_a_column_pre_ar :define_a_column + def define_a_column(i) + case do_ocicall(@ctx) { @parms[i - 1].attrGet(OCI_ATTR_DATA_TYPE) } + when 8; @stmt.defineByPos(i, String, 65535) # Read LONG values + when 187; @stmt.defineByPos(i, OraDate) # Read TIMESTAMP values + when 108 + if @parms[i - 1].attrGet(OCI_ATTR_TYPE_NAME) == 'XMLTYPE' + @stmt.defineByPos(i, String, 65535) + else + raise 'unsupported datatype' + end + else enhanced_define_a_column_pre_ar i + end + end + end + end + + if OCI8.public_method_defined?(:describe_table) + # ruby-oci8 2.0 or upper + + def describe(name) + info = describe_table(name.to_s) + raise %Q{"DESC #{name}" failed} if info.nil? + [info.obj_schema, info.obj_name] + end + else + # ruby-oci8 1.0.x or lower + + # missing constant from oci8 < 0.1.14 + OCI_PTYPE_UNK = 0 unless defined?(OCI_PTYPE_UNK) + + # Uses the describeAny OCI call to find the target owner and table_name + # indicated by +name+, parsing through synonynms as necessary. Returns + # an array of [owner, table_name]. + def describe(name) + @desc ||= @@env.alloc(OCIDescribe) + @desc.attrSet(OCI_ATTR_DESC_PUBLIC, -1) if VERSION >= '0.1.14' + do_ocicall(@ctx) { @desc.describeAny(@svc, name.to_s, OCI_PTYPE_UNK) } rescue raise %Q{"DESC #{name}" failed; does it exist?} + info = @desc.attrGet(OCI_ATTR_PARAM) + + case info.attrGet(OCI_ATTR_PTYPE) + when OCI_PTYPE_TABLE, OCI_PTYPE_VIEW + owner = info.attrGet(OCI_ATTR_OBJ_SCHEMA) + table_name = info.attrGet(OCI_ATTR_OBJ_NAME) + [owner, table_name] + when OCI_PTYPE_SYN + schema = info.attrGet(OCI_ATTR_SCHEMA_NAME) + name = info.attrGet(OCI_ATTR_NAME) + describe(schema + '.' + name) + else raise %Q{"DESC #{name}" failed; not a table or view.} + end + end + end + +end + +# The OCI8AutoRecover class enhances the OCI8 driver with auto-recover and +# reset functionality. If a call to #exec fails, and autocommit is turned on +# (ie., we're not in the middle of a longer transaction), it will +# automatically reconnect and try again. If autocommit is turned off, +# this would be dangerous (as the earlier part of the implied transaction +# may have failed silently if the connection died) -- so instead the +# connection is marked as dead, to be reconnected on it's next use. +#:stopdoc: +class OCI8EnhancedAutoRecover < DelegateClass(OCI8) #:nodoc: + attr_accessor :active #:nodoc: + alias :active? :active #:nodoc: + + cattr_accessor :auto_retry + class << self + alias :auto_retry? :auto_retry #:nodoc: + end + @@auto_retry = false + + def initialize(config, factory) #:nodoc: + @active = true + @config = config + @factory = factory + @connection = @factory.new_connection @config + super @connection + end + + # Checks connection, returns true if active. Note that ping actively + # checks the connection, while #active? simply returns the last + # known state. + def ping #:nodoc: + @connection.exec("select 1 from dual") { |r| nil } + @active = true + rescue + @active = false + raise + end + + # Resets connection, by logging off and creating a new connection. + def reset! #:nodoc: + logoff rescue nil + begin + @connection = @factory.new_connection @config + __setobj__ @connection + @active = true + rescue + @active = false + raise + end + end + + # ORA-00028: your session has been killed + # ORA-01012: not logged on + # ORA-03113: end-of-file on communication channel + # ORA-03114: not connected to ORACLE + # ORA-03135: connection lost contact + LOST_CONNECTION_ERROR_CODES = [ 28, 1012, 3113, 3114, 3135 ] #:nodoc: + + # Adds auto-recovery functionality. + # + # See: http://www.jiubao.org/ruby-oci8/api.en.html#label-11 + def exec(sql, *bindvars, &block) #:nodoc: + should_retry = self.class.auto_retry? && autocommit? + + begin + @connection.exec(sql, *bindvars, &block) + rescue OCIException => e + raise unless e.is_a?(OCIError) && LOST_CONNECTION_ERROR_CODES.include?(e.code) + @active = false + raise unless should_retry + should_retry = false + reset! rescue nil + retry + end + end + + # otherwise not working in Ruby 1.9.1 + if RUBY_VERSION =~ /^1\.9/ + def describe(name) #:nodoc: + @connection.describe(name) + end + end + +end +#:startdoc: diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-oracle_enhanced-adapter-1.4.0/lib/active_record/connection_adapters/oracle_enhanced_procedures.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-oracle_enhanced-adapter-1.4.0/lib/active_record/connection_adapters/oracle_enhanced_procedures.rb new file mode 100644 index 00000000000..6c3c325dcc6 --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-oracle_enhanced-adapter-1.4.0/lib/active_record/connection_adapters/oracle_enhanced_procedures.rb @@ -0,0 +1,260 @@ +# define accessors before requiring ruby-plsql as these accessors are used in clob writing callback and should be +# available also if ruby-plsql could not be loaded +ActiveRecord::Base.class_eval do + if respond_to? :class_attribute + class_attribute :custom_create_method, :custom_update_method, :custom_delete_method + elsif respond_to? :class_inheritable_accessor + class_inheritable_accessor :custom_create_method, :custom_update_method, :custom_delete_method + end +end + +require 'active_support' + +module ActiveRecord #:nodoc: + module ConnectionAdapters #:nodoc: + module OracleEnhancedProcedures #:nodoc: + + module ClassMethods + # Specify custom create method which should be used instead of Rails generated INSERT statement. + # Provided block should return ID of new record. + # Example: + # set_create_method do + # plsql.employees_pkg.create_employee( + # :p_first_name => first_name, + # :p_last_name => last_name, + # :p_employee_id => nil + # )[:p_employee_id] + # end + def set_create_method(&block) + include_with_custom_methods + self.custom_create_method = block + end + + # Specify custom update method which should be used instead of Rails generated UPDATE statement. + # Example: + # set_update_method do + # plsql.employees_pkg.update_employee( + # :p_employee_id => id, + # :p_first_name => first_name, + # :p_last_name => last_name + # ) + # end + def set_update_method(&block) + include_with_custom_methods + self.custom_update_method = block + end + + # Specify custom delete method which should be used instead of Rails generated DELETE statement. + # Example: + # set_delete_method do + # plsql.employees_pkg.delete_employee( + # :p_employee_id => id + # ) + # end + def set_delete_method(&block) + include_with_custom_methods + self.custom_delete_method = block + end + + if ActiveRecord::VERSION::MAJOR < 3 + def create_method_name_before_custom_methods #:nodoc: + if private_method_defined?(:create_without_timestamps) && defined?(ActiveRecord::VERSION) && ActiveRecord::VERSION::STRING.to_f >= 2.3 + :create_without_timestamps + elsif private_method_defined?(:create_without_callbacks) + :create_without_callbacks + else + :create + end + end + + def update_method_name_before_custom_methods #:nodoc: + if private_method_defined?(:update_without_dirty) + :update_without_dirty + elsif private_method_defined?(:update_without_timestamps) && defined?(ActiveRecord::VERSION) && ActiveRecord::VERSION::STRING.to_f >= 2.3 + :update_without_timestamps + elsif private_method_defined?(:update_without_callbacks) + :update_without_callbacks + else + :update + end + end + + def destroy_method_name_before_custom_methods #:nodoc: + if public_method_defined?(:destroy_without_callbacks) + :destroy_without_callbacks + else + :destroy + end + end + end + + private + + def include_with_custom_methods + unless included_modules.include? InstanceMethods + include InstanceMethods + end + end + end + + module InstanceMethods #:nodoc: + def self.included(base) + # alias methods just for ActiveRecord 2.x + # for ActiveRecord 3.0 will just redefine create, update, delete methods which call super + if ActiveRecord::VERSION::MAJOR < 3 + base.instance_eval do + alias_method :create_without_custom_method, create_method_name_before_custom_methods + alias_method create_method_name_before_custom_methods, :create_with_custom_method + alias_method :update_without_custom_method, update_method_name_before_custom_methods + alias_method update_method_name_before_custom_methods, :update_with_custom_method + alias_method :destroy_without_custom_method, destroy_method_name_before_custom_methods + alias_method destroy_method_name_before_custom_methods, :destroy_with_custom_method + private :create, :update + public :destroy + end + end + end + + if ActiveRecord::VERSION::MAJOR >= 3 + def destroy #:nodoc: + # check if class has custom delete method + if self.class.custom_delete_method + # wrap destroy in transaction + with_transaction_returning_status do + # run before/after callbacks defined in model + _run_destroy_callbacks { destroy_using_custom_method } + end + else + super + end + end + end + + private + + # Creates a record with custom create method + # and returns its id. + if ActiveRecord::VERSION::MAJOR < 3 + def create_with_custom_method + # check if class has custom create method + self.class.custom_create_method ? create_using_custom_method : create_without_custom_method + end + else # ActiveRecord 3.x + def create + # check if class has custom create method + if self.class.custom_create_method + set_timestamps_before_custom_create_method + # run before/after callbacks defined in model + _run_create_callbacks { create_using_custom_method } + else + super + end + end + end + + def create_using_custom_method + self.class.connection.log_custom_method("custom create method", "#{self.class.name} Create") do + self.id = instance_eval(&self.class.custom_create_method) + end + @new_record = false + # Starting from ActiveRecord 3.0.3 @persisted is used instead of @new_record + @persisted = true + id + end + + # Updates the associated record with custom update method + # Returns the number of affected rows. + if ActiveRecord::VERSION::MAJOR < 3 + def update_with_custom_method(attribute_names = @attributes.keys) + # check if class has custom create method + self.class.custom_update_method ? update_using_custom_method(attribute_names) : update_without_custom_method(attribute_names) + end + else # ActiveRecord 3.x + def update(attribute_names = @attributes.keys) + # check if class has custom update method + if self.class.custom_update_method + set_timestamps_before_custom_update_method + # run before/after callbacks defined in model + _run_update_callbacks do + # update just dirty attributes + if partial_updates? + # Serialized attributes should always be written in case they've been + # changed in place. + update_using_custom_method(changed | (attributes.keys & self.class.serialized_attributes.keys)) + else + update_using_custom_method(attribute_names) + end + end + else + super + end + end + end + + def update_using_custom_method(attribute_names) + return 0 if attribute_names.empty? + self.class.connection.log_custom_method("custom update method with #{self.class.primary_key}=#{self.id}", "#{self.class.name} Update") do + instance_eval(&self.class.custom_update_method) + end + 1 + end + + # Deletes the record in the database with custom delete method + # and freezes this instance to reflect that no changes should + # be made (since they can't be persisted). + if ActiveRecord::VERSION::MAJOR < 3 + def destroy_with_custom_method + # check if class has custom delete method + self.class.custom_delete_method ? destroy_using_custom_method : destroy_without_custom_method + end + end + + def destroy_using_custom_method + unless new_record? || @destroyed + self.class.connection.log_custom_method("custom delete method with #{self.class.primary_key}=#{self.id}", "#{self.class.name} Destroy") do + instance_eval(&self.class.custom_delete_method) + end + end + + @destroyed = true + freeze + end + + if ActiveRecord::VERSION::MAJOR >= 3 + def set_timestamps_before_custom_create_method + if record_timestamps + current_time = current_time_from_proper_timezone + + write_attribute('created_at', current_time) if respond_to?(:created_at) && created_at.nil? + write_attribute('created_on', current_time) if respond_to?(:created_on) && created_on.nil? + + write_attribute('updated_at', current_time) if respond_to?(:updated_at) && updated_at.nil? + write_attribute('updated_on', current_time) if respond_to?(:updated_on) && updated_on.nil? + end + end + + def set_timestamps_before_custom_update_method + if record_timestamps && (!partial_updates? || changed?) + current_time = current_time_from_proper_timezone + + write_attribute('updated_at', current_time) if respond_to?(:updated_at) + write_attribute('updated_on', current_time) if respond_to?(:updated_on) + end + end + end + + end + + end + end +end + +ActiveRecord::Base.class_eval do + extend ActiveRecord::ConnectionAdapters::OracleEnhancedProcedures::ClassMethods +end + +ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.class_eval do + # public alias to log method which could be used from other objects + alias_method :log_custom_method, :log + public :log_custom_method +end diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-oracle_enhanced-adapter-1.4.0/lib/active_record/connection_adapters/oracle_enhanced_schema_definitions.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-oracle_enhanced-adapter-1.4.0/lib/active_record/connection_adapters/oracle_enhanced_schema_definitions.rb new file mode 100644 index 00000000000..a2df72951ca --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-oracle_enhanced-adapter-1.4.0/lib/active_record/connection_adapters/oracle_enhanced_schema_definitions.rb @@ -0,0 +1,209 @@ +module ActiveRecord + module ConnectionAdapters + class OracleEnhancedForeignKeyDefinition < Struct.new(:from_table, :to_table, :options) #:nodoc: + end + + class OracleEnhancedSynonymDefinition < Struct.new(:name, :table_owner, :table_name, :db_link) #:nodoc: + end + + class OracleEnhancedIndexDefinition < Struct.new(:table, :name, :unique, :type, :parameters, :statement_parameters, + :tablespace, :columns) #:nodoc: + end + + module OracleEnhancedColumnDefinition + def self.included(base) #:nodoc: + base.class_eval do + alias_method_chain :to_sql, :virtual_columns + alias to_s :to_sql + end + end + + def to_sql_with_virtual_columns + if type==:virtual + "#{base.quote_column_name(name)} AS (#{default})" + else + to_sql_without_virtual_columns + end + end + + def lob? + ['CLOB', 'BLOB'].include?(sql_type) + end + end + + module OracleEnhancedSchemaDefinitions #:nodoc: + def self.included(base) + base::TableDefinition.class_eval do + include OracleEnhancedTableDefinition + end + + base::ColumnDefinition.class_eval do + include OracleEnhancedColumnDefinition + end + + # Available starting from ActiveRecord 2.1 + base::Table.class_eval do + include OracleEnhancedTable + end if defined?(base::Table) + end + end + + module OracleEnhancedTableDefinition + class ForeignKey < Struct.new(:base, :to_table, :options) #:nodoc: + def to_sql + base.foreign_key_definition(to_table, options) + end + alias to_s :to_sql + end + + def raw(name, options={}) + column(name, :raw, options) + end + + def self.included(base) #:nodoc: + base.class_eval do + alias_method_chain :references, :foreign_keys + alias_method_chain :to_sql, :foreign_keys + + def virtual(* args) + options = args.extract_options! + column_names = args + column_names.each { |name| column(name, :virtual, options) } + end + + end + end + + # Adds a :foreign_key option to TableDefinition.references. + # If :foreign_key is true, a foreign key constraint is added to the table. + # You can also specify a hash, which is passed as foreign key options. + # + # ===== Examples + # ====== Add goat_id column and a foreign key to the goats table. + # t.references(:goat, :foreign_key => true) + # ====== Add goat_id column and a cascading foreign key to the goats table. + # t.references(:goat, :foreign_key => {:dependent => :delete}) + # + # Note: No foreign key is created if :polymorphic => true is used. + # Note: If no name is specified, the database driver creates one for you! + def references_with_foreign_keys(*args) + options = args.extract_options! + fk_options = options.delete(:foreign_key) + + if fk_options && !options[:polymorphic] + fk_options = {} if fk_options == true + args.each { |to_table| foreign_key(to_table, fk_options) } + end + + references_without_foreign_keys(*(args << options)) + end + + # Defines a foreign key for the table. +to_table+ can be a single Symbol, or + # an Array of Symbols. See SchemaStatements#add_foreign_key + # + # ===== Examples + # ====== Creating a simple foreign key + # t.foreign_key(:people) + # ====== Defining the column + # t.foreign_key(:people, :column => :sender_id) + # ====== Creating a named foreign key + # t.foreign_key(:people, :column => :sender_id, :name => 'sender_foreign_key') + # ====== Defining the column of the +to_table+. + # t.foreign_key(:people, :column => :sender_id, :primary_key => :person_id) + def foreign_key(to_table, options = {}) + if @base.respond_to?(:supports_foreign_keys?) && @base.supports_foreign_keys? + to_table = to_table.to_s.pluralize if ActiveRecord::Base.pluralize_table_names + foreign_keys << ForeignKey.new(@base, to_table, options) + else + raise ArgumentError, "this ActiveRecord adapter is not supporting foreign_key definition" + end + end + + def to_sql_with_foreign_keys #:nodoc: + sql = to_sql_without_foreign_keys + sql << ', ' << (foreign_keys * ', ') unless foreign_keys.blank? + sql + end + + def lob_columns + columns.select(&:lob?) + end + + private + def foreign_keys + @foreign_keys ||= [] + end + end + + module OracleEnhancedTable + def self.included(base) #:nodoc: + base.class_eval do + alias_method_chain :references, :foreign_keys + end + end + + # Adds a new foreign key to the table. +to_table+ can be a single Symbol, or + # an Array of Symbols. See SchemaStatements#add_foreign_key + # + # ===== Examples + # ====== Creating a simple foreign key + # t.foreign_key(:people) + # ====== Defining the column + # t.foreign_key(:people, :column => :sender_id) + # ====== Creating a named foreign key + # t.foreign_key(:people, :column => :sender_id, :name => 'sender_foreign_key') + # ====== Defining the column of the +to_table+. + # t.foreign_key(:people, :column => :sender_id, :primary_key => :person_id) + def foreign_key(to_table, options = {}) + if @base.respond_to?(:supports_foreign_keys?) && @base.supports_foreign_keys? + to_table = to_table.to_s.pluralize if ActiveRecord::Base.pluralize_table_names + @base.add_foreign_key(@table_name, to_table, options) + else + raise ArgumentError, "this ActiveRecord adapter is not supporting foreign_key definition" + end + end + + # Remove the given foreign key from the table. + # + # ===== Examples + # ====== Remove the suppliers_company_id_fk in the suppliers table. + # t.remove_foreign_key :companies + # ====== Remove the foreign key named accounts_branch_id_fk in the accounts table. + # remove_foreign_key :column => :branch_id + # ====== Remove the foreign key named party_foreign_key in the accounts table. + # remove_index :name => :party_foreign_key + def remove_foreign_key(options = {}) + @base.remove_foreign_key(@table_name, options) + end + + # Adds a :foreign_key option to TableDefinition.references. + # If :foreign_key is true, a foreign key constraint is added to the table. + # You can also specify a hash, which is passed as foreign key options. + # + # ===== Examples + # ====== Add goat_id column and a foreign key to the goats table. + # t.references(:goat, :foreign_key => true) + # ====== Add goat_id column and a cascading foreign key to the goats table. + # t.references(:goat, :foreign_key => {:dependent => :delete}) + # + # Note: No foreign key is created if :polymorphic => true is used. + def references_with_foreign_keys(*args) + options = args.extract_options! + polymorphic = options[:polymorphic] + fk_options = options.delete(:foreign_key) + + references_without_foreign_keys(*(args << options)) + # references_without_foreign_keys adds {:type => :integer} + args.extract_options! + if fk_options && !polymorphic + fk_options = {} if fk_options == true + args.each { |to_table| foreign_key(to_table, fk_options) } + end + end + end + end +end + +ActiveRecord::ConnectionAdapters.class_eval do + include ActiveRecord::ConnectionAdapters::OracleEnhancedSchemaDefinitions +end diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-oracle_enhanced-adapter-1.4.0/lib/active_record/connection_adapters/oracle_enhanced_schema_dumper.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-oracle_enhanced-adapter-1.4.0/lib/active_record/connection_adapters/oracle_enhanced_schema_dumper.rb new file mode 100644 index 00000000000..fff24546ec2 --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-oracle_enhanced-adapter-1.4.0/lib/active_record/connection_adapters/oracle_enhanced_schema_dumper.rb @@ -0,0 +1,252 @@ +module ActiveRecord #:nodoc: + module ConnectionAdapters #:nodoc: + module OracleEnhancedSchemaDumper #:nodoc: + + def self.included(base) #:nodoc: + base.class_eval do + private + alias_method_chain :tables, :oracle_enhanced + alias_method_chain :indexes, :oracle_enhanced + end + end + + private + + def ignore_table?(table) + [ActiveRecord::Migrator.proper_table_name('schema_migrations'), ignore_tables].flatten.any? do |ignored| + case ignored + when String; table == ignored + when Regexp; table =~ ignored + else + raise StandardError, 'ActiveRecord::SchemaDumper.ignore_tables accepts an array of String and / or Regexp values.' + end + end + end + + def tables_with_oracle_enhanced(stream) + return tables_without_oracle_enhanced(stream) unless @connection.respond_to?(:materialized_views) + # do not include materialized views in schema dump - they should be created separately after schema creation + sorted_tables = (@connection.tables - @connection.materialized_views).sort + sorted_tables.each do |tbl| + # add table prefix or suffix for schema_migrations + next if ignore_table? tbl + # change table name inspect method + tbl.extend TableInspect + oracle_enhanced_table(tbl, stream) + # add primary key trigger if table has it + primary_key_trigger(tbl, stream) + end + # following table definitions + # add foreign keys if table has them + sorted_tables.each do |tbl| + next if ignore_table? tbl + foreign_keys(tbl, stream) + end + + # add synonyms in local schema + synonyms(stream) + end + + def primary_key_trigger(table_name, stream) + if @connection.respond_to?(:has_primary_key_trigger?) && @connection.has_primary_key_trigger?(table_name) + pk, pk_seq = @connection.pk_and_sequence_for(table_name) + stream.print " add_primary_key_trigger #{table_name.inspect}" + stream.print ", :primary_key => \"#{pk}\"" if pk != 'id' + stream.print "\n\n" + end + end + + def foreign_keys(table_name, stream) + if @connection.respond_to?(:foreign_keys) && (foreign_keys = @connection.foreign_keys(table_name)).any? + add_foreign_key_statements = foreign_keys.map do |foreign_key| + statement_parts = [ ('add_foreign_key ' + foreign_key.from_table.inspect) ] + statement_parts << foreign_key.to_table.inspect + + if foreign_key.options[:columns].size == 1 + column = foreign_key.options[:columns].first + if column != "#{foreign_key.to_table.singularize}_id" + statement_parts << (':column => ' + column.inspect) + end + + if foreign_key.options[:references].first != 'id' + statement_parts << (':primary_key => ' + foreign_key.options[:primary_key].inspect) + end + else + statement_parts << (':columns => ' + foreign_key.options[:columns].inspect) + end + + statement_parts << (':name => ' + foreign_key.options[:name].inspect) + + unless foreign_key.options[:dependent].blank? + statement_parts << (':dependent => ' + foreign_key.options[:dependent].inspect) + end + + ' ' + statement_parts.join(', ') + end + + stream.puts add_foreign_key_statements.sort.join("\n") + stream.puts + end + end + + def synonyms(stream) + if @connection.respond_to?(:synonyms) + syns = @connection.synonyms + syns.each do |syn| + next if ignore_table? syn.name + table_name = syn.table_name + table_name = "#{syn.table_owner}.#{table_name}" if syn.table_owner + table_name = "#{table_name}@#{syn.db_link}" if syn.db_link + stream.print " add_synonym #{syn.name.inspect}, #{table_name.inspect}, :force => true" + stream.puts + end + stream.puts unless syns.empty? + end + end + + def indexes_with_oracle_enhanced(table, stream) + # return original method if not using oracle_enhanced + if (rails_env = defined?(Rails.env) ? Rails.env : (defined?(RAILS_ENV) ? RAILS_ENV : nil)) && + ActiveRecord::Base.configurations[rails_env] && + ActiveRecord::Base.configurations[rails_env]['adapter'] != 'oracle_enhanced' + return indexes_without_oracle_enhanced(table, stream) + end + if (indexes = @connection.indexes(table)).any? + add_index_statements = indexes.map do |index| + case index.type + when nil + # use table.inspect as it will remove prefix and suffix + statement_parts = [ ('add_index ' + table.inspect) ] + statement_parts << index.columns.inspect + statement_parts << (':name => ' + index.name.inspect) + statement_parts << ':unique => true' if index.unique + statement_parts << ':tablespace => ' + index.tablespace.inspect if index.tablespace + when 'CTXSYS.CONTEXT' + if index.statement_parameters + statement_parts = [ ('add_context_index ' + table.inspect) ] + statement_parts << index.statement_parameters + else + statement_parts = [ ('add_context_index ' + table.inspect) ] + statement_parts << index.columns.inspect + statement_parts << (':name => ' + index.name.inspect) + end + else + # unrecognized index type + statement_parts = ["# unrecognized index #{index.name.inspect} with type #{index.type.inspect}"] + end + ' ' + statement_parts.join(', ') + end + + stream.puts add_index_statements.sort.join("\n") + stream.puts + end + end + + def oracle_enhanced_table(table, stream) + columns = @connection.columns(table) + begin + tbl = StringIO.new + + # first dump primary key column + if @connection.respond_to?(:pk_and_sequence_for) + pk, pk_seq = @connection.pk_and_sequence_for(table) + elsif @connection.respond_to?(:primary_key) + pk = @connection.primary_key(table) + end + + tbl.print " create_table #{table.inspect}" + + # addition to make temporary option work + tbl.print ", :temporary => true" if @connection.temporary_table?(table) + + if columns.detect { |c| c.name == pk } + if pk != 'id' + tbl.print %Q(, :primary_key => "#{pk}") + end + else + tbl.print ", :id => false" + end + tbl.print ", :force => true" + tbl.puts " do |t|" + + # then dump all non-primary key columns + column_specs = columns.map do |column| + raise StandardError, "Unknown type '#{column.sql_type}' for column '#{column.name}'" if @types[column.type].nil? + next if column.name == pk + spec = {} + spec[:name] = column.name.inspect + spec[:type] = column.virtual? ? 'virtual' : column.type.to_s + spec[:limit] = column.limit.inspect if column.limit != @types[column.type][:limit] && column.type != :decimal + spec[:precision] = column.precision.inspect if !column.precision.nil? + spec[:scale] = column.scale.inspect if !column.scale.nil? + spec[:null] = 'false' if !column.null + spec[:default] = column.virtual_column_data_default if column.virtual? + spec[:default] ||= default_string(column.default) if column.has_default? + (spec.keys - [:name, :type]).each{ |k| spec[k].insert(0, "#{k.inspect} => ")} + spec + end.compact + + # find all migration keys used in this table + keys = [:name, :limit, :precision, :scale, :default, :null] & column_specs.map(&:keys).flatten + + # figure out the lengths for each column based on above keys + lengths = keys.map{ |key| column_specs.map{ |spec| spec[key] ? spec[key].length + 2 : 0 }.max } + + # the string we're going to sprintf our values against, with standardized column widths + format_string = lengths.map{ |len| "%-#{len}s" } + + # find the max length for the 'type' column, which is special + type_length = column_specs.map{ |column| column[:type].length }.max + + # add column type definition to our format string + format_string.unshift " t.%-#{type_length}s " + + format_string *= '' + + column_specs.each do |colspec| + values = keys.zip(lengths).map{ |key, len| colspec.key?(key) ? colspec[key] + ", " : " " * len } + values.unshift colspec[:type] + tbl.print((format_string % values).gsub(/,\s*$/, '')) + tbl.puts + end + + tbl.puts " end" + tbl.puts + + indexes(table, tbl) + + tbl.rewind + stream.print tbl.read + rescue => e + stream.puts "# Could not dump table #{table.inspect} because of following #{e.class}" + stream.puts "# #{e.message}" + stream.puts + end + + stream + end + + + # remove table name prefix and suffix when doing #inspect (which is used in tables method) + module TableInspect #:nodoc: + def inspect + remove_prefix_and_suffix(self) + end + + private + def remove_prefix_and_suffix(table_name) + if table_name =~ /\A#{ActiveRecord::Base.table_name_prefix.to_s.gsub('$','\$')}(.*)#{ActiveRecord::Base.table_name_suffix.to_s.gsub('$','\$')}\Z/ + "\"#{$1}\"" + else + "\"#{table_name}\"" + end + end + end + + end + end +end + +ActiveRecord::SchemaDumper.class_eval do + include ActiveRecord::ConnectionAdapters::OracleEnhancedSchemaDumper +end diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-oracle_enhanced-adapter-1.4.0/lib/active_record/connection_adapters/oracle_enhanced_schema_statements.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-oracle_enhanced-adapter-1.4.0/lib/active_record/connection_adapters/oracle_enhanced_schema_statements.rb new file mode 100644 index 00000000000..86e24d04859 --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-oracle_enhanced-adapter-1.4.0/lib/active_record/connection_adapters/oracle_enhanced_schema_statements.rb @@ -0,0 +1,374 @@ +require 'digest/sha1' + +module ActiveRecord + module ConnectionAdapters + module OracleEnhancedSchemaStatements + # SCHEMA STATEMENTS ======================================== + # + # see: abstract/schema_statements.rb + + # Additional options for +create_table+ method in migration files. + # + # You can specify individual starting value in table creation migration file, e.g.: + # + # create_table :users, :sequence_start_value => 100 do |t| + # # ... + # end + # + # You can also specify other sequence definition additional parameters, e.g.: + # + # create_table :users, :sequence_start_value => “100 NOCACHE INCREMENT BY 10†do |t| + # # ... + # end + # + # Create primary key trigger (so that you can skip primary key value in INSERT statement). + # By default trigger name will be "table_name_pkt", you can override the name with + # :trigger_name option (but it is not recommended to override it as then this trigger will + # not be detected by ActiveRecord model and it will still do prefetching of sequence value). + # Example: + # + # create_table :users, :primary_key_trigger => true do |t| + # # ... + # end + # + # It is possible to add table and column comments in table creation migration files: + # + # create_table :employees, :comment => “Employees and contractors†do |t| + # t.string :first_name, :comment => “Given name†+ # t.string :last_name, :comment => “Surname†+ # end + + def create_table(name, options = {}, &block) + create_sequence = options[:id] != false + column_comments = {} + + table_definition = TableDefinition.new(self) + table_definition.primary_key(options[:primary_key] || Base.get_primary_key(name.to_s.singularize)) unless options[:id] == false + + # store that primary key was defined in create_table block + unless create_sequence + class << table_definition + attr_accessor :create_sequence + def primary_key(*args) + self.create_sequence = true + super(*args) + end + end + end + + # store column comments + class << table_definition + attr_accessor :column_comments + def column(name, type, options = {}) + if options[:comment] + self.column_comments ||= {} + self.column_comments[name] = options[:comment] + end + super(name, type, options) + end + end + + result = block.call(table_definition) if block + create_sequence = create_sequence || table_definition.create_sequence + column_comments = table_definition.column_comments if table_definition.column_comments + tablespace = tablespace_for(:table, options[:tablespace]) + + if options[:force] && table_exists?(name) + drop_table(name, options) + end + + create_sql = "CREATE#{' GLOBAL TEMPORARY' if options[:temporary]} TABLE " + create_sql << quote_table_name(name) + create_sql << " (#{table_definition.to_sql})" + unless options[:temporary] + create_sql << " ORGANIZATION #{options[:organization]}" if options[:organization] + create_sql << tablespace + table_definition.lob_columns.each{|cd| create_sql << tablespace_for(cd.sql_type.downcase.to_sym, nil, name, cd.name)} + end + create_sql << " #{options[:options]}" + execute create_sql + + create_sequence_and_trigger(name, options) if create_sequence + + add_table_comment name, options[:comment] + column_comments.each do |column_name, comment| + add_comment name, column_name, comment + end + + end + + def rename_table(name, new_name) #:nodoc: + execute "RENAME #{quote_table_name(name)} TO #{quote_table_name(new_name)}" + execute "RENAME #{quote_table_name("#{name}_seq")} TO #{quote_table_name("#{new_name}_seq")}" rescue nil + end + + def drop_table(name, options = {}) #:nodoc: + super(name) + seq_name = options[:sequence_name] || default_sequence_name(name) + execute "DROP SEQUENCE #{quote_table_name(seq_name)}" rescue nil + ensure + clear_table_columns_cache(name) + end + + # clear cached indexes when adding new index + def add_index(table_name, column_name, options = {}) #:nodoc: + column_names = Array(column_name) + # sonar - see below + index_name = nil + # /sonar + + if Hash === options # legacy support, since this param was a string + index_type = options[:unique] ? "UNIQUE" : "" + index_name = options[:name].to_s if options.key?(:name) + tablespace = tablespace_for(:index, options[:tablespace]) + else + index_type = options + end + + # sonar - move the call to index_name() in order to remove the log "Oracle enhanced shortened index name" even if the index name + # is explicitly set by migrations with the :name option + index_name = index_name(table_name, :column => column_names) unless index_name + # /sonar + + if index_name.to_s.length > index_name_length + raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' is too long; the limit is #{index_name_length} characters" + end + if index_name_exists?(table_name, index_name, false) + raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' already exists" + end + quoted_column_names = column_names.map { |e| quote_column_name_or_expression(e) }.join(", ") + + execute "CREATE #{index_type} INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} (#{quoted_column_names})#{tablespace} #{options[:options]}" + ensure + self.all_schema_indexes = nil + end + + # Remove the given index from the table. + # Gives warning if index does not exist + def remove_index(table_name, options = {}) #:nodoc: + index_name = index_name(table_name, options) + unless index_name_exists?(table_name, index_name, true) + raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' does not exist" + end + remove_index!(table_name, index_name) + end + + # clear cached indexes when removing index + def remove_index!(table_name, index_name) #:nodoc: + execute "DROP INDEX #{quote_column_name(index_name)}" + ensure + self.all_schema_indexes = nil + end + + # returned shortened index name if default is too large + def index_name(table_name, options) #:nodoc: + default_name = super(table_name, options).to_s + # sometimes options can be String or Array with column names + options = {} unless options.is_a?(Hash) + identifier_max_length = options[:identifier_max_length] || index_name_length + return default_name if default_name.length <= identifier_max_length + + # remove 'index', 'on' and 'and' keywords + shortened_name = "i_#{table_name}_#{Array(options[:column]) * '_'}" + + # leave just first three letters from each word + if shortened_name.length > identifier_max_length + shortened_name = shortened_name.split('_').map{|w| w[0,3]}.join('_') + end + # generate unique name using hash function + if shortened_name.length > identifier_max_length + shortened_name = 'i'+Digest::SHA1.hexdigest(default_name)[0,identifier_max_length-1] + end + @logger.warn "#{adapter_name} shortened default index name #{default_name} to #{shortened_name}" if @logger + shortened_name + end + + # Verify the existence of an index with a given name. + # + # The default argument is returned if the underlying implementation does not define the indexes method, + # as there's no way to determine the correct answer in that case. + # + # Will always query database and not index cache. + def index_name_exists?(table_name, index_name, default) + (owner, table_name, db_link) = @connection.describe(table_name) + result = select_value(<<-SQL) + SELECT 1 FROM all_indexes#{db_link} i + WHERE i.owner = '#{owner}' + AND i.table_owner = '#{owner}' + AND i.table_name = '#{table_name}' + AND i.index_name = '#{index_name.to_s.upcase}' + SQL + result == 1 + end + + def rename_index(table_name, index_name, new_index_name) #:nodoc: + unless index_name_exists?(table_name, index_name, true) + raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' does not exist" + end + execute "ALTER INDEX #{quote_column_name(index_name)} rename to #{quote_column_name(new_index_name)}" + ensure + self.all_schema_indexes = nil + end + + def add_column(table_name, column_name, type, options = {}) #:nodoc: + add_column_sql = "ALTER TABLE #{quote_table_name(table_name)} ADD #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}" + add_column_options!(add_column_sql, options.merge(:type=>type, :column_name=>column_name, :table_name=>table_name)) + add_column_sql << tablespace_for((type_to_sql(type).downcase.to_sym), nil, table_name, column_name) + execute(add_column_sql) + ensure + clear_table_columns_cache(table_name) + end + + def change_column_default(table_name, column_name, default) #:nodoc: + execute "ALTER TABLE #{quote_table_name(table_name)} MODIFY #{quote_column_name(column_name)} DEFAULT #{quote(default)}" + ensure + clear_table_columns_cache(table_name) + end + + def change_column_null(table_name, column_name, null, default = nil) #:nodoc: + column = column_for(table_name, column_name) + + unless null || default.nil? + execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL") + end + + change_column table_name, column_name, column.sql_type, :null => null + end + + def change_column(table_name, column_name, type, options = {}) #:nodoc: + column = column_for(table_name, column_name) + + # remove :null option if its value is the same as current column definition + # otherwise Oracle will raise error + if options.has_key?(:null) && options[:null] == column.null + options[:null] = nil + end + + change_column_sql = "ALTER TABLE #{quote_table_name(table_name)} MODIFY #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}" + add_column_options!(change_column_sql, options.merge(:type=>type, :column_name=>column_name, :table_name=>table_name)) + change_column_sql << tablespace_for((type_to_sql(type).downcase.to_sym), nil, options[:table_name], options[:column_name]) + execute(change_column_sql) + ensure + clear_table_columns_cache(table_name) + end + + def rename_column(table_name, column_name, new_column_name) #:nodoc: + execute "ALTER TABLE #{quote_table_name(table_name)} RENAME COLUMN #{quote_column_name(column_name)} to #{quote_column_name(new_column_name)}" + ensure + clear_table_columns_cache(table_name) + end + + def remove_column(table_name, column_name) #:nodoc: + execute "ALTER TABLE #{quote_table_name(table_name)} DROP COLUMN #{quote_column_name(column_name)}" + ensure + clear_table_columns_cache(table_name) + end + + def add_comment(table_name, column_name, comment) #:nodoc: + return if comment.blank? + execute "COMMENT ON COLUMN #{quote_table_name(table_name)}.#{column_name} IS '#{comment}'" + end + + def add_table_comment(table_name, comment) #:nodoc: + return if comment.blank? + execute "COMMENT ON TABLE #{quote_table_name(table_name)} IS '#{comment}'" + end + + def table_comment(table_name) #:nodoc: + (owner, table_name, db_link) = @connection.describe(table_name) + select_value <<-SQL + SELECT comments FROM all_tab_comments#{db_link} + WHERE owner = '#{owner}' + AND table_name = '#{table_name}' + SQL + end + + def column_comment(table_name, column_name) #:nodoc: + (owner, table_name, db_link) = @connection.describe(table_name) + select_value <<-SQL + SELECT comments FROM all_col_comments#{db_link} + WHERE owner = '#{owner}' + AND table_name = '#{table_name}' + AND column_name = '#{column_name.upcase}' + SQL + end + + # Maps logical Rails types to Oracle-specific data types. + def type_to_sql(type, limit = nil, precision = nil, scale = nil) #:nodoc: + # Ignore options for :text and :binary columns + return super(type, nil, nil, nil) if ['text', 'binary'].include?(type.to_s) + + super + end + + def tablespace(table_name) + select_value <<-SQL + SELECT tablespace_name + FROM user_tables + WHERE table_name='#{table_name.to_s.upcase}' + SQL + end + + private + + def tablespace_for(obj_type, tablespace_option, table_name=nil, column_name=nil) + tablespace_sql = '' + if tablespace = (tablespace_option || default_tablespace_for(obj_type)) + tablespace_sql << if [:blob, :clob].include?(obj_type.to_sym) + " LOB (#{column_name}) STORE AS #{column_name.to_s[0..10]}_#{table_name.to_s[0..14]}_ls (TABLESPACE #{tablespace})" + else + " TABLESPACE #{tablespace}" + end + end + tablespace_sql + end + + def default_tablespace_for(type) + (default_tablespaces[type] || default_tablespaces[native_database_types[type][:name]]) rescue nil + end + + + def column_for(table_name, column_name) + unless column = columns(table_name).find { |c| c.name == column_name.to_s } + raise "No such column: #{table_name}.#{column_name}" + end + column + end + + def create_sequence_and_trigger(table_name, options) + seq_name = options[:sequence_name] || default_sequence_name(table_name) + seq_start_value = options[:sequence_start_value] || default_sequence_start_value + execute "CREATE SEQUENCE #{quote_table_name(seq_name)} START WITH #{seq_start_value}" + + create_primary_key_trigger(table_name, options) if options[:primary_key_trigger] + end + + def create_primary_key_trigger(table_name, options) + seq_name = options[:sequence_name] || default_sequence_name(table_name) + trigger_name = options[:trigger_name] || default_trigger_name(table_name) + primary_key = options[:primary_key] || Base.get_primary_key(table_name.to_s.singularize) + execute compress_lines(<<-SQL) + CREATE OR REPLACE TRIGGER #{quote_table_name(trigger_name)} + BEFORE INSERT ON #{quote_table_name(table_name)} FOR EACH ROW + BEGIN + IF inserting THEN + IF :new.#{quote_column_name(primary_key)} IS NULL THEN + SELECT #{quote_table_name(seq_name)}.NEXTVAL INTO :new.#{quote_column_name(primary_key)} FROM dual; + END IF; + END IF; + END; + SQL + end + + def default_trigger_name(table_name) + # truncate table name if necessary to fit in max length of identifier + "#{table_name.to_s[0,table_name_length-4]}_pkt" + end + + end + end +end + +ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.class_eval do + include ActiveRecord::ConnectionAdapters::OracleEnhancedSchemaStatements +end diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-oracle_enhanced-adapter-1.4.0/lib/active_record/connection_adapters/oracle_enhanced_schema_statements_ext.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-oracle_enhanced-adapter-1.4.0/lib/active_record/connection_adapters/oracle_enhanced_schema_statements_ext.rb new file mode 100644 index 00000000000..39b7b18ff35 --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-oracle_enhanced-adapter-1.4.0/lib/active_record/connection_adapters/oracle_enhanced_schema_statements_ext.rb @@ -0,0 +1,256 @@ +require 'digest/sha1' + +module ActiveRecord + module ConnectionAdapters + module OracleEnhancedSchemaStatementsExt + def supports_foreign_keys? #:nodoc: + true + end + + # Create primary key trigger (so that you can skip primary key value in INSERT statement). + # By default trigger name will be "table_name_pkt", you can override the name with + # :trigger_name option (but it is not recommended to override it as then this trigger will + # not be detected by ActiveRecord model and it will still do prefetching of sequence value). + # + # add_primary_key_trigger :users + # + # You can also create primary key trigger using +create_table+ with :primary_key_trigger + # option: + # + # create_table :users, :primary_key_trigger => true do |t| + # # ... + # end + # + def add_primary_key_trigger(table_name, options={}) + # call the same private method that is used for create_table :primary_key_trigger => true + create_primary_key_trigger(table_name, options) + end + + # Adds a new foreign key to the +from_table+, referencing the primary key of +to_table+ + # (syntax and partial implementation taken from http://github.com/matthuhiggins/foreigner) + # + # The foreign key will be named after the from and to tables unless you pass + # <tt>:name</tt> as an option. + # + # === Examples + # ==== Creating a foreign key + # add_foreign_key(:comments, :posts) + # generates + # ALTER TABLE comments ADD CONSTRAINT + # comments_post_id_fk FOREIGN KEY (post_id) REFERENCES posts (id) + # + # ==== Creating a named foreign key + # add_foreign_key(:comments, :posts, :name => 'comments_belongs_to_posts') + # generates + # ALTER TABLE comments ADD CONSTRAINT + # comments_belongs_to_posts FOREIGN KEY (post_id) REFERENCES posts (id) + # + # ==== Creating a cascading foreign_key on a custom column + # add_foreign_key(:people, :people, :column => 'best_friend_id', :dependent => :nullify) + # generates + # ALTER TABLE people ADD CONSTRAINT + # people_best_friend_id_fk FOREIGN KEY (best_friend_id) REFERENCES people (id) + # ON DELETE SET NULL + # + # ==== Creating a composite foreign key + # add_foreign_key(:comments, :posts, :columns => ['post_id', 'author_id'], :name => 'comments_post_fk') + # generates + # ALTER TABLE comments ADD CONSTRAINT + # comments_post_fk FOREIGN KEY (post_id, author_id) REFERENCES posts (post_id, author_id) + # + # === Supported options + # [:column] + # Specify the column name on the from_table that references the to_table. By default this is guessed + # to be the singular name of the to_table with "_id" suffixed. So a to_table of :posts will use "post_id" + # as the default <tt>:column</tt>. + # [:columns] + # An array of column names when defining composite foreign keys. An alias of <tt>:column</tt> provided for improved readability. + # [:primary_key] + # Specify the column name on the to_table that is referenced by this foreign key. By default this is + # assumed to be "id". Ignored when defining composite foreign keys. + # [:name] + # Specify the name of the foreign key constraint. This defaults to use from_table and foreign key column. + # [:dependent] + # If set to <tt>:delete</tt>, the associated records in from_table are deleted when records in to_table table are deleted. + # If set to <tt>:nullify</tt>, the foreign key column is set to +NULL+. + def add_foreign_key(from_table, to_table, options = {}) + columns = options[:column] || options[:columns] || "#{to_table.to_s.singularize}_id" + constraint_name = foreign_key_constraint_name(from_table, columns, options) + sql = "ALTER TABLE #{quote_table_name(from_table)} ADD CONSTRAINT #{quote_column_name(constraint_name)} " + sql << foreign_key_definition(to_table, options) + execute sql + end + + def foreign_key_definition(to_table, options = {}) #:nodoc: + columns = Array(options[:column] || options[:columns]) + + if columns.size > 1 + # composite foreign key + columns_sql = columns.map {|c| quote_column_name(c)}.join(',') + references = options[:references] || columns + references_sql = references.map {|c| quote_column_name(c)}.join(',') + else + columns_sql = quote_column_name(columns.first || "#{to_table.to_s.singularize}_id") + references = options[:references] ? options[:references].first : nil + references_sql = quote_column_name(options[:primary_key] || references || "id") + end + + sql = "FOREIGN KEY (#{columns_sql}) REFERENCES #{quote_table_name(to_table)}(#{references_sql})" + + case options[:dependent] + when :nullify + sql << " ON DELETE SET NULL" + when :delete + sql << " ON DELETE CASCADE" + end + sql + end + + # Remove the given foreign key from the table. + # + # ===== Examples + # ====== Remove the suppliers_company_id_fk in the suppliers table. + # remove_foreign_key :suppliers, :companies + # ====== Remove the foreign key named accounts_branch_id_fk in the accounts table. + # remove_foreign_key :accounts, :column => :branch_id + # ====== Remove the foreign key named party_foreign_key in the accounts table. + # remove_foreign_key :accounts, :name => :party_foreign_key + def remove_foreign_key(from_table, options) + if Hash === options + constraint_name = foreign_key_constraint_name(from_table, options[:column], options) + else + constraint_name = foreign_key_constraint_name(from_table, "#{options.to_s.singularize}_id") + end + execute "ALTER TABLE #{quote_table_name(from_table)} DROP CONSTRAINT #{quote_column_name(constraint_name)}" + end + + private + + def foreign_key_constraint_name(table_name, columns, options = {}) + columns = Array(columns) + constraint_name = original_name = options[:name] || "#{table_name}_#{columns.join('_')}_fk" + + return constraint_name if constraint_name.length <= OracleEnhancedAdapter::IDENTIFIER_MAX_LENGTH + + # leave just first three letters from each word + constraint_name = constraint_name.split('_').map{|w| w[0,3]}.join('_') + # generate unique name using hash function + if constraint_name.length > OracleEnhancedAdapter::IDENTIFIER_MAX_LENGTH + constraint_name = 'c'+Digest::SHA1.hexdigest(original_name)[0,OracleEnhancedAdapter::IDENTIFIER_MAX_LENGTH-1] + end + @logger.warn "#{adapter_name} shortened foreign key constraint name #{original_name} to #{constraint_name}" if @logger + constraint_name + end + + + public + + # get table foreign keys for schema dump + def foreign_keys(table_name) #:nodoc: + (owner, desc_table_name, db_link) = @connection.describe(table_name) + + fk_info = select_all(<<-SQL, 'Foreign Keys') + SELECT r.table_name to_table + ,rc.column_name references_column + ,cc.column_name + ,c.constraint_name name + ,c.delete_rule + FROM user_constraints#{db_link} c, user_cons_columns#{db_link} cc, + user_constraints#{db_link} r, user_cons_columns#{db_link} rc + WHERE c.owner = '#{owner}' + AND c.table_name = '#{desc_table_name}' + AND c.constraint_type = 'R' + AND cc.owner = c.owner + AND cc.constraint_name = c.constraint_name + AND r.constraint_name = c.r_constraint_name + AND r.owner = c.owner + AND rc.owner = r.owner + AND rc.constraint_name = r.constraint_name + AND rc.position = cc.position + ORDER BY name, to_table, column_name, references_column + SQL + + fks = {} + + fk_info.map do |row| + name = oracle_downcase(row['name']) + fks[name] ||= { :columns => [], :to_table => oracle_downcase(row['to_table']), :references => [] } + fks[name][:columns] << oracle_downcase(row['column_name']) + fks[name][:references] << oracle_downcase(row['references_column']) + case row['delete_rule'] + when 'CASCADE' + fks[name][:dependent] = :delete + when 'SET NULL' + fks[name][:dependent] = :nullify + end + end + + fks.map do |k, v| + options = {:name => k, :columns => v[:columns], :references => v[:references], :dependent => v[:dependent]} + OracleEnhancedForeignKeyDefinition.new(table_name, v[:to_table], options) + end + end + + # REFERENTIAL INTEGRITY ==================================== + + def disable_referential_integrity(&block) #:nodoc: + sql_constraints = <<-SQL + SELECT constraint_name, owner, table_name + FROM user_constraints + WHERE constraint_type = 'R' + AND status = 'ENABLED' + SQL + old_constraints = select_all(sql_constraints) + begin + old_constraints.each do |constraint| + execute "ALTER TABLE #{constraint["table_name"]} DISABLE CONSTRAINT #{constraint["constraint_name"]}" + end + yield + ensure + old_constraints.each do |constraint| + execute "ALTER TABLE #{constraint["table_name"]} ENABLE CONSTRAINT #{constraint["constraint_name"]}" + end + end + end + + # Add synonym to existing table or view or sequence. Can be used to create local synonym to + # remote table in other schema or in other database + # Examples: + # + # add_synonym :posts, "blog.posts" + # add_synonym :posts_seq, "blog.posts_seq" + # add_synonym :employees, "hr.employees@dblink", :force => true + # + def add_synonym(name, table_name, options = {}) + sql = "CREATE" + if options[:force] == true + sql << " OR REPLACE" + end + sql << " SYNONYM #{quote_table_name(name)} FOR #{quote_table_name(table_name)}" + execute sql + end + + # Remove existing synonym to table or view or sequence + # Example: + # + # remove_synonym :posts, "blog.posts" + # + def remove_synonym(name) + execute "DROP SYNONYM #{quote_table_name(name)}" + end + + # get synonyms for schema dump + def synonyms #:nodoc: + select_all("SELECT synonym_name, table_owner, table_name, db_link FROM user_synonyms").collect do |row| + OracleEnhancedSynonymDefinition.new(oracle_downcase(row['synonym_name']), + oracle_downcase(row['table_owner']), oracle_downcase(row['table_name']), oracle_downcase(row['db_link'])) + end + end + + end + end +end + +ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.class_eval do + include ActiveRecord::ConnectionAdapters::OracleEnhancedSchemaStatementsExt +end diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-oracle_enhanced-adapter-1.4.0/lib/active_record/connection_adapters/oracle_enhanced_structure_dump.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-oracle_enhanced-adapter-1.4.0/lib/active_record/connection_adapters/oracle_enhanced_structure_dump.rb new file mode 100644 index 00000000000..f4507e7154b --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-oracle_enhanced-adapter-1.4.0/lib/active_record/connection_adapters/oracle_enhanced_structure_dump.rb @@ -0,0 +1,290 @@ +module ActiveRecord #:nodoc: + module ConnectionAdapters #:nodoc: + module OracleEnhancedStructureDump #:nodoc: + + # Statements separator used in structure dump to allow loading of structure dump also with SQL*Plus + STATEMENT_TOKEN = "\n\n/\n\n" + + def structure_dump #:nodoc: + structure = select_values("SELECT sequence_name FROM user_sequences ORDER BY 1").map do |seq| + "CREATE SEQUENCE \"#{seq}\"" + end + select_values("SELECT table_name FROM all_tables t + WHERE owner = SYS_CONTEXT('userenv', 'session_user') AND secondary = 'N' + AND NOT EXISTS (SELECT mv.mview_name FROM all_mviews mv WHERE mv.owner = t.owner AND mv.mview_name = t.table_name) + AND NOT EXISTS (SELECT mvl.log_table FROM all_mview_logs mvl WHERE mvl.log_owner = t.owner AND mvl.log_table = t.table_name) + ORDER BY 1").each do |table_name| + virtual_columns = virtual_columns_for(table_name) + ddl = "CREATE#{ ' GLOBAL TEMPORARY' if temporary_table?(table_name)} TABLE \"#{table_name}\" (\n" + cols = select_all(%Q{ + SELECT column_name, data_type, data_length, char_used, char_length, data_precision, data_scale, data_default, nullable + FROM user_tab_columns + WHERE table_name = '#{table_name}' + ORDER BY column_id + }).map do |row| + if(v = virtual_columns.find {|col| col['column_name'] == row['column_name']}) + structure_dump_virtual_column(row, v['data_default']) + else + structure_dump_column(row) + end + end + ddl << cols.join(",\n ") + ddl << structure_dump_primary_key(table_name) + ddl << "\n)" + structure << ddl + structure << structure_dump_indexes(table_name) + structure << structure_dump_unique_keys(table_name) + end + + join_with_statement_token(structure) << structure_dump_fk_constraints + end + + def structure_dump_column(column) #:nodoc: + col = "\"#{column['column_name']}\" #{column['data_type']}" + if column['data_type'] =='NUMBER' and !column['data_precision'].nil? + col << "(#{column['data_precision'].to_i}" + col << ",#{column['data_scale'].to_i}" if !column['data_scale'].nil? + col << ')' + elsif column['data_type'].include?('CHAR') + length = column['char_used'] == 'C' ? column['char_length'].to_i : column['data_length'].to_i + col << "(#{length})" + end + col << " DEFAULT #{column['data_default']}" if !column['data_default'].nil? + col << ' NOT NULL' if column['nullable'] == 'N' + col + end + + def structure_dump_virtual_column(column, data_default) #:nodoc: + data_default = data_default.gsub(/"/, '') + col = "\"#{column['column_name']}\" #{column['data_type']}" + if column['data_type'] =='NUMBER' and !column['data_precision'].nil? + col << "(#{column['data_precision'].to_i}" + col << ",#{column['data_scale'].to_i}" if !column['data_scale'].nil? + col << ')' + elsif column['data_type'].include?('CHAR') + length = column['char_used'] == 'C' ? column['char_length'].to_i : column['data_length'].to_i + col << "(#{length})" + end + col << " GENERATED ALWAYS AS (#{data_default}) VIRTUAL" + end + + def structure_dump_primary_key(table) #:nodoc: + opts = {:name => '', :cols => []} + pks = select_all(<<-SQL, "Primary Keys") + SELECT a.constraint_name, a.column_name, a.position + FROM user_cons_columns a + JOIN user_constraints c + ON a.constraint_name = c.constraint_name + WHERE c.table_name = '#{table.upcase}' + AND c.constraint_type = 'P' + AND c.owner = SYS_CONTEXT('userenv', 'session_user') + SQL + pks.each do |row| + opts[:name] = row['constraint_name'] + opts[:cols][row['position']-1] = row['column_name'] + end + opts[:cols].length > 0 ? ",\n CONSTRAINT #{opts[:name]} PRIMARY KEY (#{opts[:cols].join(',')})" : '' + end + + def structure_dump_unique_keys(table) #:nodoc: + keys = {} + uks = select_all(<<-SQL, "Primary Keys") + SELECT a.constraint_name, a.column_name, a.position + FROM user_cons_columns a + JOIN user_constraints c + ON a.constraint_name = c.constraint_name + WHERE c.table_name = '#{table.upcase}' + AND c.constraint_type = 'U' + AND c.owner = SYS_CONTEXT('userenv', 'session_user') + SQL + uks.each do |uk| + keys[uk['constraint_name']] ||= [] + keys[uk['constraint_name']][uk['position']-1] = uk['column_name'] + end + keys.map do |k,v| + "ALTER TABLE #{table.upcase} ADD CONSTRAINT #{k} UNIQUE (#{v.join(',')})" + end + end + + def structure_dump_indexes(table_name) #:nodoc: + indexes(table_name).map do |options| + column_names = options[:columns] + options = {:name => options[:name], :unique => options[:unique]} + index_name = index_name(table_name, :column => column_names) + if Hash === options # legacy support, since this param was a string + index_type = options[:unique] ? "UNIQUE" : "" + index_name = options[:name] || index_name + else + index_type = options + end + quoted_column_names = column_names.map { |e| quote_column_name(e) }.join(", ") + "CREATE #{index_type} INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} (#{quoted_column_names})" + end + end + + def structure_dump_fk_constraints #:nodoc: + fks = select_all("SELECT table_name FROM all_tables WHERE owner = SYS_CONTEXT('userenv', 'session_user') ORDER BY 1").map do |table| + if respond_to?(:foreign_keys) && (foreign_keys = foreign_keys(table["table_name"])).any? + foreign_keys.map do |fk| + sql = "ALTER TABLE #{quote_table_name(fk.from_table)} ADD CONSTRAINT #{quote_column_name(fk.options[:name])} " + sql << "#{foreign_key_definition(fk.to_table, fk.options)}" + end + end + end.flatten.compact + join_with_statement_token(fks) + end + + def dump_schema_information #:nodoc: + sm_table = ActiveRecord::Migrator.schema_migrations_table_name + migrated = select_values("SELECT version FROM #{sm_table}") + join_with_statement_token(migrated.map{|v| "INSERT INTO #{sm_table} (version) VALUES ('#{v}')" }) + end + + # Extract all stored procedures, packages, synonyms and views. + def structure_dump_db_stored_code #:nodoc: + structure = [] + select_all("SELECT DISTINCT name, type + FROM all_source + WHERE type IN ('PROCEDURE', 'PACKAGE', 'PACKAGE BODY', 'FUNCTION', 'TRIGGER', 'TYPE') + AND name NOT LIKE 'BIN$%' + AND owner = SYS_CONTEXT('userenv', 'session_user') ORDER BY type").each do |source| + ddl = "CREATE OR REPLACE \n" + lines = select_all(%Q{ + SELECT text + FROM all_source + WHERE name = '#{source['name']}' + AND type = '#{source['type']}' + AND owner = SYS_CONTEXT('userenv', 'session_user') + ORDER BY line + }).map do |row| + ddl << row['text'] + end + ddl << ";" unless ddl.strip[-1,1] == ";" + structure << ddl + end + + # export views + select_all("SELECT view_name, text FROM user_views").each do |view| + structure << "CREATE OR REPLACE VIEW #{view['view_name']} AS\n #{view['text']}" + end + + # export synonyms + select_all("SELECT owner, synonym_name, table_name, table_owner + FROM all_synonyms + WHERE owner = SYS_CONTEXT('userenv', 'session_user') ").each do |synonym| + structure << "CREATE OR REPLACE #{synonym['owner'] == 'PUBLIC' ? 'PUBLIC' : '' } SYNONYM #{synonym['synonym_name']}" + structure << " FOR #{synonym['table_owner']}.#{synonym['table_name']}" + end + + join_with_statement_token(structure) + end + + def structure_drop #:nodoc: + statements = select_values("SELECT sequence_name FROM user_sequences ORDER BY 1").map do |seq| + "DROP SEQUENCE \"#{seq}\"" + end + select_values("SELECT table_name from all_tables t + WHERE owner = SYS_CONTEXT('userenv', 'session_user') AND secondary = 'N' + AND NOT EXISTS (SELECT mv.mview_name FROM all_mviews mv WHERE mv.owner = t.owner AND mv.mview_name = t.table_name) + AND NOT EXISTS (SELECT mvl.log_table FROM all_mview_logs mvl WHERE mvl.log_owner = t.owner AND mvl.log_table = t.table_name) + ORDER BY 1").each do |table| + statements << "DROP TABLE \"#{table}\" CASCADE CONSTRAINTS" + end + join_with_statement_token(statements) + end + + def temp_table_drop #:nodoc: + join_with_statement_token(select_values( + "SELECT table_name FROM all_tables + WHERE owner = SYS_CONTEXT('userenv', 'session_user') AND secondary = 'N' AND temporary = 'Y' ORDER BY 1").map do |table| + "DROP TABLE \"#{table}\" CASCADE CONSTRAINTS" + end) + end + + def full_drop(preserve_tables=false) #:nodoc: + s = preserve_tables ? [] : [structure_drop] + s << temp_table_drop if preserve_tables + s << drop_sql_for_feature("view") + s << drop_sql_for_feature("materialized view") + s << drop_sql_for_feature("synonym") + s << drop_sql_for_feature("type") + s << drop_sql_for_object("package") + s << drop_sql_for_object("function") + s << drop_sql_for_object("procedure") + s.join + end + + def add_column_options!(sql, options) #:nodoc: + type = options[:type] || ((column = options[:column]) && column.type) + type = type && type.to_sym + # handle case of defaults for CLOB columns, which would otherwise get "quoted" incorrectly + if options_include_default?(options) + if type == :text + sql << " DEFAULT #{quote(options[:default])}" + else + # from abstract adapter + sql << " DEFAULT #{quote(options[:default], options[:column])}" + end + end + # must explicitly add NULL or NOT NULL to allow change_column to work on migrations + if options[:null] == false + sql << " NOT NULL" + elsif options[:null] == true + sql << " NULL" unless type == :primary_key + end + end + + def execute_structure_dump(string) + string.split(STATEMENT_TOKEN).each do |ddl| + ddl.chop! if ddl.last == ";" + execute(ddl) unless ddl.blank? + end + end + + private + + # virtual columns are an 11g feature. This returns [] if feature is not + # present or none are found. + # return [{'column_name' => 'FOOS', 'data_default' => '...'}, ...] + def virtual_columns_for(table) + begin + select_all <<-SQL + SELECT column_name, data_default + FROM user_tab_cols + WHERE virtual_column = 'YES' + AND table_name = '#{table.upcase}' + SQL + # feature not supported previous to 11g + rescue ActiveRecord::StatementInvalid => e + [] + end + end + + def drop_sql_for_feature(type) + short_type = type == 'materialized view' ? 'mview' : type + join_with_statement_token( + select_values("SELECT #{short_type}_name FROM user_#{short_type.tableize}").map do |name| + "DROP #{type.upcase} \"#{name}\"" + end) + end + + def drop_sql_for_object(type) + join_with_statement_token( + select_values("SELECT object_name FROM user_objects WHERE object_type = '#{type.upcase}'").map do |name| + "DROP #{type.upcase} \"#{name}\"" + end) + end + + def join_with_statement_token(array) + string = array.join(STATEMENT_TOKEN) + string << STATEMENT_TOKEN unless string.blank? + string + end + + end + end +end + +ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.class_eval do + include ActiveRecord::ConnectionAdapters::OracleEnhancedStructureDump +end diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-oracle_enhanced-adapter-1.4.0/lib/active_record/connection_adapters/oracle_enhanced_tasks.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-oracle_enhanced-adapter-1.4.0/lib/active_record/connection_adapters/oracle_enhanced_tasks.rb new file mode 100644 index 00000000000..2b16cf645e4 --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-oracle_enhanced-adapter-1.4.0/lib/active_record/connection_adapters/oracle_enhanced_tasks.rb @@ -0,0 +1,17 @@ +# Used just for Rails 2.x +# In Rails 3.x rake tasks are loaded using railtie +if ActiveRecord::VERSION::MAJOR == 2 + + if defined?(Rake.application) && Rake.application && + ActiveRecord::Base.configurations[defined?(Rails.env) ? Rails.env : RAILS_ENV]['adapter'] == 'oracle_enhanced' + oracle_enhanced_rakefile = File.dirname(__FILE__) + "/oracle_enhanced.rake" + if Rake.application.lookup("environment") + # rails tasks already defined; load the override tasks now + load oracle_enhanced_rakefile + else + # rails tasks not loaded yet; load as an import + Rake.application.add_import(oracle_enhanced_rakefile) + end + end + +end diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-oracle_enhanced-adapter-1.4.0/lib/active_record/connection_adapters/oracle_enhanced_version.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-oracle_enhanced-adapter-1.4.0/lib/active_record/connection_adapters/oracle_enhanced_version.rb new file mode 100644 index 00000000000..2f55d1e54a6 --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-oracle_enhanced-adapter-1.4.0/lib/active_record/connection_adapters/oracle_enhanced_version.rb @@ -0,0 +1,4 @@ +#sonar +#ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter::VERSION = File.read(File.dirname(__FILE__)+'/../../../VERSION').chomp +ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter::VERSION = '1.4.0' +#/sonar diff --git a/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-oracle_enhanced-adapter-1.4.0/lib/activerecord-oracle_enhanced-adapter.rb b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-oracle_enhanced-adapter-1.4.0/lib/activerecord-oracle_enhanced-adapter.rb new file mode 100644 index 00000000000..69e13b54230 --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/vendor/gems/activerecord-oracle_enhanced-adapter-1.4.0/lib/activerecord-oracle_enhanced-adapter.rb @@ -0,0 +1,25 @@ +# define railtie which will be executed in Rails 3 +if defined?(::Rails::Railtie) + + module ActiveRecord + module ConnectionAdapters + class OracleEnhancedRailtie < ::Rails::Railtie + rake_tasks do + load 'active_record/connection_adapters/oracle_enhanced.rake' + end + + ActiveSupport.on_load(:active_record) do + require 'active_record/connection_adapters/oracle_enhanced_adapter' + + # Cache column descriptions between requests in test and production environments + if Rails.env.test? || Rails.env.production? + ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.cache_columns = true + end + + end + + end + end + end + +end
\ No newline at end of file diff --git a/sonar-server/src/main/webapp/javascripts/calendar/calendar-min.js b/sonar-server/src/main/webapp/javascripts/calendar/calendar-min.js new file mode 100644 index 00000000000..0d8406af862 --- /dev/null +++ b/sonar-server/src/main/webapp/javascripts/calendar/calendar-min.js @@ -0,0 +1,1514 @@ +(function(){YAHOO.util.Config=function(D){if(D){this.init(D) +}}; +var B=YAHOO.lang,C=YAHOO.util.CustomEvent,A=YAHOO.util.Config; +A.CONFIG_CHANGED_EVENT="configChanged"; +A.BOOLEAN_TYPE="boolean"; +A.prototype={owner:null,queueInProgress:false,config:null,initialConfig:null,eventQueue:null,configChangedEvent:null,init:function(D){this.owner=D; +this.configChangedEvent=this.createEvent(A.CONFIG_CHANGED_EVENT); +this.configChangedEvent.signature=C.LIST; +this.queueInProgress=false; +this.config={}; +this.initialConfig={}; +this.eventQueue=[] +},checkBoolean:function(D){return(typeof D==A.BOOLEAN_TYPE) +},checkNumber:function(D){return(!isNaN(D)) +},fireEvent:function(D,F){var E=this.config[D]; +if(E&&E.event){E.event.fire(F) +}},addProperty:function(E,D){E=E.toLowerCase(); +this.config[E]=D; +D.event=this.createEvent(E,{scope:this.owner}); +D.event.signature=C.LIST; +D.key=E; +if(D.handler){D.event.subscribe(D.handler,this.owner) +}this.setProperty(E,D.value,true); +if(!D.suppressEvent){this.queueProperty(E,D.value) +}},getConfig:function(){var D={},F,E; +for(F in this.config){E=this.config[F]; +if(E&&E.event){D[F]=E.value +}}return D +},getProperty:function(D){var E=this.config[D.toLowerCase()]; +if(E&&E.event){return E.value +}else{return undefined +}},resetProperty:function(D){D=D.toLowerCase(); +var E=this.config[D]; +if(E&&E.event){if(this.initialConfig[D]&&!B.isUndefined(this.initialConfig[D])){this.setProperty(D,this.initialConfig[D]); +return true +}}else{return false +}},setProperty:function(E,G,D){var F; +E=E.toLowerCase(); +if(this.queueInProgress&&!D){this.queueProperty(E,G); +return true +}else{F=this.config[E]; +if(F&&F.event){if(F.validator&&!F.validator(G)){return false +}else{F.value=G; +if(!D){this.fireEvent(E,G); +this.configChangedEvent.fire([E,G]) +}return true +}}else{return false +}}},queueProperty:function(S,P){S=S.toLowerCase(); +var R=this.config[S],K=false,J,G,H,I,O,Q,F,M,N,D,L,T,E; +if(R&&R.event){if(!B.isUndefined(P)&&R.validator&&!R.validator(P)){return false +}else{if(!B.isUndefined(P)){R.value=P +}else{P=R.value +}K=false; +J=this.eventQueue.length; +for(L=0; +L<J; +L++){G=this.eventQueue[L]; +if(G){H=G[0]; +I=G[1]; +if(H==S){this.eventQueue[L]=null; +this.eventQueue.push([S,(!B.isUndefined(P)?P:I)]); +K=true; +break +}}}if(!K&&!B.isUndefined(P)){this.eventQueue.push([S,P]) +}}if(R.supercedes){O=R.supercedes.length; +for(T=0; +T<O; +T++){Q=R.supercedes[T]; +F=this.eventQueue.length; +for(E=0; +E<F; +E++){M=this.eventQueue[E]; +if(M){N=M[0]; +D=M[1]; +if(N==Q.toLowerCase()){this.eventQueue.push([N,D]); +this.eventQueue[E]=null; +break +}}}}}return true +}else{return false +}},refireEvent:function(D){D=D.toLowerCase(); +var E=this.config[D]; +if(E&&E.event&&!B.isUndefined(E.value)){if(this.queueInProgress){this.queueProperty(D) +}else{this.fireEvent(D,E.value) +}}},applyConfig:function(D,G){var F,E; +if(G){E={}; +for(F in D){if(B.hasOwnProperty(D,F)){E[F.toLowerCase()]=D[F] +}}this.initialConfig=E +}for(F in D){if(B.hasOwnProperty(D,F)){this.queueProperty(F,D[F]) +}}},refresh:function(){var D; +for(D in this.config){this.refireEvent(D) +}},fireQueue:function(){var E,H,D,G,F; +this.queueInProgress=true; +for(E=0; +E<this.eventQueue.length; +E++){H=this.eventQueue[E]; +if(H){D=H[0]; +G=H[1]; +F=this.config[D]; +F.value=G; +this.fireEvent(D,G) +}}this.queueInProgress=false; +this.eventQueue=[] +},subscribeToConfigEvent:function(E,F,H,D){var G=this.config[E.toLowerCase()]; +if(G&&G.event){if(!A.alreadySubscribed(G.event,F,H)){G.event.subscribe(F,H,D) +}return true +}else{return false +}},unsubscribeFromConfigEvent:function(D,E,G){var F=this.config[D.toLowerCase()]; +if(F&&F.event){return F.event.unsubscribe(E,G) +}else{return false +}},toString:function(){var D="Config"; +if(this.owner){D+=" ["+this.owner.toString()+"]" +}return D +},outputEventQueue:function(){var D="",G,E,F=this.eventQueue.length; +for(E=0; +E<F; +E++){G=this.eventQueue[E]; +if(G){D+=G[0]+"="+G[1]+", " +}}return D +},destroy:function(){var E=this.config,D,F; +for(D in E){if(B.hasOwnProperty(E,D)){F=E[D]; +F.event.unsubscribeAll(); +F.event=null +}}this.configChangedEvent.unsubscribeAll(); +this.configChangedEvent=null; +this.owner=null; +this.config=null; +this.initialConfig=null; +this.eventQueue=null +}}; +A.alreadySubscribed=function(E,H,I){var F=E.subscribers.length,D,G; +if(F>0){G=F-1; +do{D=E.subscribers[G]; +if(D&&D.obj==I&&D.fn==H){return true +}}while(G--) +}return false +}; +YAHOO.lang.augmentProto(A,YAHOO.util.EventProvider) +}()); +YAHOO.widget.DateMath={DAY:"D",WEEK:"W",YEAR:"Y",MONTH:"M",ONE_DAY_MS:1000*60*60*24,add:function(A,D,C){var F=new Date(A.getTime()); +switch(D){case this.MONTH:var E=A.getMonth()+C; +var B=0; +if(E<0){while(E<0){E+=12; +B-=1 +}}else{if(E>11){while(E>11){E-=12; +B+=1 +}}}F.setMonth(E); +F.setFullYear(A.getFullYear()+B); +break; +case this.DAY:F.setDate(A.getDate()+C); +break; +case this.YEAR:F.setFullYear(A.getFullYear()+C); +break; +case this.WEEK:F.setDate(A.getDate()+(C*7)); +break +}return F +},subtract:function(A,C,B){return this.add(A,C,(B*-1)) +},before:function(C,B){var A=B.getTime(); +if(C.getTime()<A){return true +}else{return false +}},after:function(C,B){var A=B.getTime(); +if(C.getTime()>A){return true +}else{return false +}},between:function(B,A,C){if(this.after(B,A)&&this.before(B,C)){return true +}else{return false +}},getJan1:function(A){return this.getDate(A,0,1) +},getDayOffset:function(B,D){var C=this.getJan1(D); +var A=Math.ceil((B.getTime()-C.getTime())/this.ONE_DAY_MS); +return A +},getWeekNumber:function(C,F){C=this.clearTime(C); +var E=new Date(C.getTime()+(4*this.ONE_DAY_MS)-((C.getDay())*this.ONE_DAY_MS)); +var B=this.getDate(E.getFullYear(),0,1); +var A=((E.getTime()-B.getTime())/this.ONE_DAY_MS)-1; +var D=Math.ceil((A)/7); +return D +},isYearOverlapWeek:function(A){var C=false; +var B=this.add(A,this.DAY,6); +if(B.getFullYear()!=A.getFullYear()){C=true +}return C +},isMonthOverlapWeek:function(A){var C=false; +var B=this.add(A,this.DAY,6); +if(B.getMonth()!=A.getMonth()){C=true +}return C +},findMonthStart:function(A){var B=this.getDate(A.getFullYear(),A.getMonth(),1); +return B +},findMonthEnd:function(B){var D=this.findMonthStart(B); +var C=this.add(D,this.MONTH,1); +var A=this.subtract(C,this.DAY,1); +return A +},clearTime:function(A){A.setHours(12,0,0,0); +return A +},getDate:function(D,A,C){var B=null; +if(YAHOO.lang.isUndefined(C)){C=1 +}if(D>=100){B=new Date(D,A,C) +}else{B=new Date(); +B.setFullYear(D); +B.setMonth(A); +B.setDate(C); +B.setHours(0,0,0,0) +}return B +}}; +YAHOO.widget.Calendar=function(C,A,B){this.init.apply(this,arguments) +}; +YAHOO.widget.Calendar.IMG_ROOT=null; +YAHOO.widget.Calendar.DATE="D"; +YAHOO.widget.Calendar.MONTH_DAY="MD"; +YAHOO.widget.Calendar.WEEKDAY="WD"; +YAHOO.widget.Calendar.RANGE="R"; +YAHOO.widget.Calendar.MONTH="M"; +YAHOO.widget.Calendar.DISPLAY_DAYS=42; +YAHOO.widget.Calendar.STOP_RENDER="S"; +YAHOO.widget.Calendar.SHORT="short"; +YAHOO.widget.Calendar.LONG="long"; +YAHOO.widget.Calendar.MEDIUM="medium"; +YAHOO.widget.Calendar.ONE_CHAR="1char"; +YAHOO.widget.Calendar._DEFAULT_CONFIG={PAGEDATE:{key:"pagedate",value:null},SELECTED:{key:"selected",value:null},TITLE:{key:"title",value:""},CLOSE:{key:"close",value:false},IFRAME:{key:"iframe",value:(YAHOO.env.ua.ie&&YAHOO.env.ua.ie<=6)?true:false},MINDATE:{key:"mindate",value:null},MAXDATE:{key:"maxdate",value:null},MULTI_SELECT:{key:"multi_select",value:false},START_WEEKDAY:{key:"start_weekday",value:0},SHOW_WEEKDAYS:{key:"show_weekdays",value:true},SHOW_WEEK_HEADER:{key:"show_week_header",value:false},SHOW_WEEK_FOOTER:{key:"show_week_footer",value:false},HIDE_BLANK_WEEKS:{key:"hide_blank_weeks",value:false},NAV_ARROW_LEFT:{key:"nav_arrow_left",value:null},NAV_ARROW_RIGHT:{key:"nav_arrow_right",value:null},MONTHS_SHORT:{key:"months_short",value:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]},MONTHS_LONG:{key:"months_long",value:["January","February","March","April","May","June","July","August","September","October","November","December"]},WEEKDAYS_1CHAR:{key:"weekdays_1char",value:["S","M","T","W","T","F","S"]},WEEKDAYS_SHORT:{key:"weekdays_short",value:["Su","Mo","Tu","We","Th","Fr","Sa"]},WEEKDAYS_MEDIUM:{key:"weekdays_medium",value:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"]},WEEKDAYS_LONG:{key:"weekdays_long",value:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"]},LOCALE_MONTHS:{key:"locale_months",value:"long"},LOCALE_WEEKDAYS:{key:"locale_weekdays",value:"short"},DATE_DELIMITER:{key:"date_delimiter",value:","},DATE_FIELD_DELIMITER:{key:"date_field_delimiter",value:"/"},DATE_RANGE_DELIMITER:{key:"date_range_delimiter",value:"-"},MY_MONTH_POSITION:{key:"my_month_position",value:1},MY_YEAR_POSITION:{key:"my_year_position",value:2},MD_MONTH_POSITION:{key:"md_month_position",value:1},MD_DAY_POSITION:{key:"md_day_position",value:2},MDY_MONTH_POSITION:{key:"mdy_month_position",value:1},MDY_DAY_POSITION:{key:"mdy_day_position",value:2},MDY_YEAR_POSITION:{key:"mdy_year_position",value:3},MY_LABEL_MONTH_POSITION:{key:"my_label_month_position",value:1},MY_LABEL_YEAR_POSITION:{key:"my_label_year_position",value:2},MY_LABEL_MONTH_SUFFIX:{key:"my_label_month_suffix",value:" "},MY_LABEL_YEAR_SUFFIX:{key:"my_label_year_suffix",value:""},NAV:{key:"navigator",value:null}}; +YAHOO.widget.Calendar._EVENT_TYPES={BEFORE_SELECT:"beforeSelect",SELECT:"select",BEFORE_DESELECT:"beforeDeselect",DESELECT:"deselect",CHANGE_PAGE:"changePage",BEFORE_RENDER:"beforeRender",RENDER:"render",RESET:"reset",CLEAR:"clear",BEFORE_HIDE:"beforeHide",HIDE:"hide",BEFORE_SHOW:"beforeShow",SHOW:"show",BEFORE_HIDE_NAV:"beforeHideNav",HIDE_NAV:"hideNav",BEFORE_SHOW_NAV:"beforeShowNav",SHOW_NAV:"showNav",BEFORE_RENDER_NAV:"beforeRenderNav",RENDER_NAV:"renderNav"}; +YAHOO.widget.Calendar._STYLES={CSS_ROW_HEADER:"calrowhead",CSS_ROW_FOOTER:"calrowfoot",CSS_CELL:"calcell",CSS_CELL_SELECTOR:"selector",CSS_CELL_SELECTED:"selected",CSS_CELL_SELECTABLE:"selectable",CSS_CELL_RESTRICTED:"restricted",CSS_CELL_TODAY:"today",CSS_CELL_OOM:"oom",CSS_CELL_OOB:"previous",CSS_HEADER:"calheader",CSS_HEADER_TEXT:"calhead",CSS_BODY:"calbody",CSS_WEEKDAY_CELL:"calweekdaycell",CSS_WEEKDAY_ROW:"calweekdayrow",CSS_FOOTER:"calfoot",CSS_CALENDAR:"yui-calendar",CSS_SINGLE:"single",CSS_CONTAINER:"yui-calcontainer",CSS_NAV_LEFT:"calnavleft",CSS_NAV_RIGHT:"calnavright",CSS_NAV:"calnav",CSS_CLOSE:"calclose",CSS_CELL_TOP:"calcelltop",CSS_CELL_LEFT:"calcellleft",CSS_CELL_RIGHT:"calcellright",CSS_CELL_BOTTOM:"calcellbottom",CSS_CELL_HOVER:"calcellhover",CSS_CELL_HIGHLIGHT1:"highlight1",CSS_CELL_HIGHLIGHT2:"highlight2",CSS_CELL_HIGHLIGHT3:"highlight3",CSS_CELL_HIGHLIGHT4:"highlight4"}; +YAHOO.widget.Calendar.prototype={Config:null,parent:null,index:-1,cells:null,cellDates:null,id:null,containerId:null,oDomContainer:null,today:null,renderStack:null,_renderStack:null,oNavigator:null,_selectedDates:null,domEventMap:null,_parseArgs:function(B){var A={id:null,container:null,config:null}; +if(B&&B.length&&B.length>0){switch(B.length){case 1:A.id=null; +A.container=B[0]; +A.config=null; +break; +case 2:if(YAHOO.lang.isObject(B[1])&&!B[1].tagName&&!(B[1] instanceof String)){A.id=null; +A.container=B[0]; +A.config=B[1] +}else{A.id=B[0]; +A.container=B[1]; +A.config=null +}break; +default:A.id=B[0]; +A.container=B[1]; +A.config=B[2]; +break +}}else{}return A +},init:function(D,B,C){var A=this._parseArgs(arguments); +D=A.id; +B=A.container; +C=A.config; +this.oDomContainer=YAHOO.util.Dom.get(B); +if(!this.oDomContainer.id){this.oDomContainer.id=YAHOO.util.Dom.generateId() +}if(!D){D=this.oDomContainer.id+"_t" +}this.id=D; +this.containerId=this.oDomContainer.id; +this.initEvents(); +this.today=new Date(); +YAHOO.widget.DateMath.clearTime(this.today); +this.cfg=new YAHOO.util.Config(this); +this.Options={}; +this.Locale={}; +this.initStyles(); +YAHOO.util.Dom.addClass(this.oDomContainer,this.Style.CSS_CONTAINER); +YAHOO.util.Dom.addClass(this.oDomContainer,this.Style.CSS_SINGLE); +this.cellDates=[]; +this.cells=[]; +this.renderStack=[]; +this._renderStack=[]; +this.setupConfig(); +if(C){this.cfg.applyConfig(C,true) +}this.cfg.fireQueue() +},configIframe:function(C,B,D){var A=B[0]; +if(!this.parent){if(YAHOO.util.Dom.inDocument(this.oDomContainer)){if(A){var E=YAHOO.util.Dom.getStyle(this.oDomContainer,"position"); +if(E=="absolute"||E=="relative"){if(!YAHOO.util.Dom.inDocument(this.iframe)){this.iframe=document.createElement("iframe"); +this.iframe.src="javascript:false;"; +YAHOO.util.Dom.setStyle(this.iframe,"opacity","0"); +if(YAHOO.env.ua.ie&&YAHOO.env.ua.ie<=6){YAHOO.util.Dom.addClass(this.iframe,"fixedsize") +}this.oDomContainer.insertBefore(this.iframe,this.oDomContainer.firstChild) +}}}else{if(this.iframe){if(this.iframe.parentNode){this.iframe.parentNode.removeChild(this.iframe) +}this.iframe=null +}}}}},configTitle:function(B,A,C){var E=A[0]; +if(E){this.createTitleBar(E) +}else{var D=this.cfg.getProperty(YAHOO.widget.Calendar._DEFAULT_CONFIG.CLOSE.key); +if(!D){this.removeTitleBar() +}else{this.createTitleBar(" ") +}}},configClose:function(B,A,C){var E=A[0],D=this.cfg.getProperty(YAHOO.widget.Calendar._DEFAULT_CONFIG.TITLE.key); +if(E){if(!D){this.createTitleBar(" ") +}this.createCloseButton() +}else{this.removeCloseButton(); +if(!D){this.removeTitleBar() +}}},initEvents:function(){var A=YAHOO.widget.Calendar._EVENT_TYPES; +this.beforeSelectEvent=new YAHOO.util.CustomEvent(A.BEFORE_SELECT); +this.selectEvent=new YAHOO.util.CustomEvent(A.SELECT); +this.beforeDeselectEvent=new YAHOO.util.CustomEvent(A.BEFORE_DESELECT); +this.deselectEvent=new YAHOO.util.CustomEvent(A.DESELECT); +this.changePageEvent=new YAHOO.util.CustomEvent(A.CHANGE_PAGE); +this.beforeRenderEvent=new YAHOO.util.CustomEvent(A.BEFORE_RENDER); +this.renderEvent=new YAHOO.util.CustomEvent(A.RENDER); +this.resetEvent=new YAHOO.util.CustomEvent(A.RESET); +this.clearEvent=new YAHOO.util.CustomEvent(A.CLEAR); +this.beforeShowEvent=new YAHOO.util.CustomEvent(A.BEFORE_SHOW); +this.showEvent=new YAHOO.util.CustomEvent(A.SHOW); +this.beforeHideEvent=new YAHOO.util.CustomEvent(A.BEFORE_HIDE); +this.hideEvent=new YAHOO.util.CustomEvent(A.HIDE); +this.beforeShowNavEvent=new YAHOO.util.CustomEvent(A.BEFORE_SHOW_NAV); +this.showNavEvent=new YAHOO.util.CustomEvent(A.SHOW_NAV); +this.beforeHideNavEvent=new YAHOO.util.CustomEvent(A.BEFORE_HIDE_NAV); +this.hideNavEvent=new YAHOO.util.CustomEvent(A.HIDE_NAV); +this.beforeRenderNavEvent=new YAHOO.util.CustomEvent(A.BEFORE_RENDER_NAV); +this.renderNavEvent=new YAHOO.util.CustomEvent(A.RENDER_NAV); +this.beforeSelectEvent.subscribe(this.onBeforeSelect,this,true); +this.selectEvent.subscribe(this.onSelect,this,true); +this.beforeDeselectEvent.subscribe(this.onBeforeDeselect,this,true); +this.deselectEvent.subscribe(this.onDeselect,this,true); +this.changePageEvent.subscribe(this.onChangePage,this,true); +this.renderEvent.subscribe(this.onRender,this,true); +this.resetEvent.subscribe(this.onReset,this,true); +this.clearEvent.subscribe(this.onClear,this,true) +},doSelectCell:function(G,A){var L,F,I,C; +var H=YAHOO.util.Event.getTarget(G); +var B=H.tagName.toLowerCase(); +var E=false; +while(B!="td"&&!YAHOO.util.Dom.hasClass(H,A.Style.CSS_CELL_SELECTABLE)){if(!E&&B=="a"&&YAHOO.util.Dom.hasClass(H,A.Style.CSS_CELL_SELECTOR)){E=true +}H=H.parentNode; +B=H.tagName.toLowerCase(); +if(B=="html"){return +}}if(E){YAHOO.util.Event.preventDefault(G) +}L=H; +if(YAHOO.util.Dom.hasClass(L,A.Style.CSS_CELL_SELECTABLE)){F=L.id.split("cell")[1]; +I=A.cellDates[F]; +C=YAHOO.widget.DateMath.getDate(I[0],I[1]-1,I[2]); +var K; +if(A.Options.MULTI_SELECT){K=L.getElementsByTagName("a")[0]; +if(K){K.blur() +}var D=A.cellDates[F]; +var J=A._indexOfSelectedFieldArray(D); +if(J>-1){A.deselectCell(F) +}else{A.selectCell(F) +}}else{K=L.getElementsByTagName("a")[0]; +if(K){K.blur() +}A.selectCell(F) +}}},doCellMouseOver:function(C,B){var A; +if(C){A=YAHOO.util.Event.getTarget(C) +}else{A=this +}while(A.tagName&&A.tagName.toLowerCase()!="td"){A=A.parentNode; +if(!A.tagName||A.tagName.toLowerCase()=="html"){return +}}if(YAHOO.util.Dom.hasClass(A,B.Style.CSS_CELL_SELECTABLE)){YAHOO.util.Dom.addClass(A,B.Style.CSS_CELL_HOVER) +}},doCellMouseOut:function(C,B){var A; +if(C){A=YAHOO.util.Event.getTarget(C) +}else{A=this +}while(A.tagName&&A.tagName.toLowerCase()!="td"){A=A.parentNode; +if(!A.tagName||A.tagName.toLowerCase()=="html"){return +}}if(YAHOO.util.Dom.hasClass(A,B.Style.CSS_CELL_SELECTABLE)){YAHOO.util.Dom.removeClass(A,B.Style.CSS_CELL_HOVER) +}},setupConfig:function(){var A=YAHOO.widget.Calendar._DEFAULT_CONFIG; +this.cfg.addProperty(A.PAGEDATE.key,{value:new Date(),handler:this.configPageDate}); +this.cfg.addProperty(A.SELECTED.key,{value:[],handler:this.configSelected}); +this.cfg.addProperty(A.TITLE.key,{value:A.TITLE.value,handler:this.configTitle}); +this.cfg.addProperty(A.CLOSE.key,{value:A.CLOSE.value,handler:this.configClose}); +this.cfg.addProperty(A.IFRAME.key,{value:A.IFRAME.value,handler:this.configIframe,validator:this.cfg.checkBoolean}); +this.cfg.addProperty(A.MINDATE.key,{value:A.MINDATE.value,handler:this.configMinDate}); +this.cfg.addProperty(A.MAXDATE.key,{value:A.MAXDATE.value,handler:this.configMaxDate}); +this.cfg.addProperty(A.MULTI_SELECT.key,{value:A.MULTI_SELECT.value,handler:this.configOptions,validator:this.cfg.checkBoolean}); +this.cfg.addProperty(A.START_WEEKDAY.key,{value:A.START_WEEKDAY.value,handler:this.configOptions,validator:this.cfg.checkNumber}); +this.cfg.addProperty(A.SHOW_WEEKDAYS.key,{value:A.SHOW_WEEKDAYS.value,handler:this.configOptions,validator:this.cfg.checkBoolean}); +this.cfg.addProperty(A.SHOW_WEEK_HEADER.key,{value:A.SHOW_WEEK_HEADER.value,handler:this.configOptions,validator:this.cfg.checkBoolean}); +this.cfg.addProperty(A.SHOW_WEEK_FOOTER.key,{value:A.SHOW_WEEK_FOOTER.value,handler:this.configOptions,validator:this.cfg.checkBoolean}); +this.cfg.addProperty(A.HIDE_BLANK_WEEKS.key,{value:A.HIDE_BLANK_WEEKS.value,handler:this.configOptions,validator:this.cfg.checkBoolean}); +this.cfg.addProperty(A.NAV_ARROW_LEFT.key,{value:A.NAV_ARROW_LEFT.value,handler:this.configOptions}); +this.cfg.addProperty(A.NAV_ARROW_RIGHT.key,{value:A.NAV_ARROW_RIGHT.value,handler:this.configOptions}); +this.cfg.addProperty(A.MONTHS_SHORT.key,{value:A.MONTHS_SHORT.value,handler:this.configLocale}); +this.cfg.addProperty(A.MONTHS_LONG.key,{value:A.MONTHS_LONG.value,handler:this.configLocale}); +this.cfg.addProperty(A.WEEKDAYS_1CHAR.key,{value:A.WEEKDAYS_1CHAR.value,handler:this.configLocale}); +this.cfg.addProperty(A.WEEKDAYS_SHORT.key,{value:A.WEEKDAYS_SHORT.value,handler:this.configLocale}); +this.cfg.addProperty(A.WEEKDAYS_MEDIUM.key,{value:A.WEEKDAYS_MEDIUM.value,handler:this.configLocale}); +this.cfg.addProperty(A.WEEKDAYS_LONG.key,{value:A.WEEKDAYS_LONG.value,handler:this.configLocale}); +var B=function(){this.cfg.refireEvent(A.LOCALE_MONTHS.key); +this.cfg.refireEvent(A.LOCALE_WEEKDAYS.key) +}; +this.cfg.subscribeToConfigEvent(A.START_WEEKDAY.key,B,this,true); +this.cfg.subscribeToConfigEvent(A.MONTHS_SHORT.key,B,this,true); +this.cfg.subscribeToConfigEvent(A.MONTHS_LONG.key,B,this,true); +this.cfg.subscribeToConfigEvent(A.WEEKDAYS_1CHAR.key,B,this,true); +this.cfg.subscribeToConfigEvent(A.WEEKDAYS_SHORT.key,B,this,true); +this.cfg.subscribeToConfigEvent(A.WEEKDAYS_MEDIUM.key,B,this,true); +this.cfg.subscribeToConfigEvent(A.WEEKDAYS_LONG.key,B,this,true); +this.cfg.addProperty(A.LOCALE_MONTHS.key,{value:A.LOCALE_MONTHS.value,handler:this.configLocaleValues}); +this.cfg.addProperty(A.LOCALE_WEEKDAYS.key,{value:A.LOCALE_WEEKDAYS.value,handler:this.configLocaleValues}); +this.cfg.addProperty(A.DATE_DELIMITER.key,{value:A.DATE_DELIMITER.value,handler:this.configLocale}); +this.cfg.addProperty(A.DATE_FIELD_DELIMITER.key,{value:A.DATE_FIELD_DELIMITER.value,handler:this.configLocale}); +this.cfg.addProperty(A.DATE_RANGE_DELIMITER.key,{value:A.DATE_RANGE_DELIMITER.value,handler:this.configLocale}); +this.cfg.addProperty(A.MY_MONTH_POSITION.key,{value:A.MY_MONTH_POSITION.value,handler:this.configLocale,validator:this.cfg.checkNumber}); +this.cfg.addProperty(A.MY_YEAR_POSITION.key,{value:A.MY_YEAR_POSITION.value,handler:this.configLocale,validator:this.cfg.checkNumber}); +this.cfg.addProperty(A.MD_MONTH_POSITION.key,{value:A.MD_MONTH_POSITION.value,handler:this.configLocale,validator:this.cfg.checkNumber}); +this.cfg.addProperty(A.MD_DAY_POSITION.key,{value:A.MD_DAY_POSITION.value,handler:this.configLocale,validator:this.cfg.checkNumber}); +this.cfg.addProperty(A.MDY_MONTH_POSITION.key,{value:A.MDY_MONTH_POSITION.value,handler:this.configLocale,validator:this.cfg.checkNumber}); +this.cfg.addProperty(A.MDY_DAY_POSITION.key,{value:A.MDY_DAY_POSITION.value,handler:this.configLocale,validator:this.cfg.checkNumber}); +this.cfg.addProperty(A.MDY_YEAR_POSITION.key,{value:A.MDY_YEAR_POSITION.value,handler:this.configLocale,validator:this.cfg.checkNumber}); +this.cfg.addProperty(A.MY_LABEL_MONTH_POSITION.key,{value:A.MY_LABEL_MONTH_POSITION.value,handler:this.configLocale,validator:this.cfg.checkNumber}); +this.cfg.addProperty(A.MY_LABEL_YEAR_POSITION.key,{value:A.MY_LABEL_YEAR_POSITION.value,handler:this.configLocale,validator:this.cfg.checkNumber}); +this.cfg.addProperty(A.MY_LABEL_MONTH_SUFFIX.key,{value:A.MY_LABEL_MONTH_SUFFIX.value,handler:this.configLocale}); +this.cfg.addProperty(A.MY_LABEL_YEAR_SUFFIX.key,{value:A.MY_LABEL_YEAR_SUFFIX.value,handler:this.configLocale}); +this.cfg.addProperty(A.NAV.key,{value:A.NAV.value,handler:this.configNavigator}) +},configPageDate:function(B,A,C){this.cfg.setProperty(YAHOO.widget.Calendar._DEFAULT_CONFIG.PAGEDATE.key,this._parsePageDate(A[0]),true) +},configMinDate:function(B,A,C){var D=A[0]; +if(YAHOO.lang.isString(D)){D=this._parseDate(D); +this.cfg.setProperty(YAHOO.widget.Calendar._DEFAULT_CONFIG.MINDATE.key,YAHOO.widget.DateMath.getDate(D[0],(D[1]-1),D[2])) +}},configMaxDate:function(B,A,C){var D=A[0]; +if(YAHOO.lang.isString(D)){D=this._parseDate(D); +this.cfg.setProperty(YAHOO.widget.Calendar._DEFAULT_CONFIG.MAXDATE.key,YAHOO.widget.DateMath.getDate(D[0],(D[1]-1),D[2])) +}},configSelected:function(C,A,E){var B=A[0]; +var D=YAHOO.widget.Calendar._DEFAULT_CONFIG.SELECTED.key; +if(B){if(YAHOO.lang.isString(B)){this.cfg.setProperty(D,this._parseDates(B),true) +}}if(!this._selectedDates){this._selectedDates=this.cfg.getProperty(D) +}},configOptions:function(B,A,C){this.Options[B.toUpperCase()]=A[0] +},configLocale:function(C,B,D){var A=YAHOO.widget.Calendar._DEFAULT_CONFIG; +this.Locale[C.toUpperCase()]=B[0]; +this.cfg.refireEvent(A.LOCALE_MONTHS.key); +this.cfg.refireEvent(A.LOCALE_WEEKDAYS.key) +},configLocaleValues:function(D,C,E){var B=YAHOO.widget.Calendar._DEFAULT_CONFIG; +D=D.toLowerCase(); +var G=C[0]; +switch(D){case B.LOCALE_MONTHS.key:switch(G){case YAHOO.widget.Calendar.SHORT:this.Locale.LOCALE_MONTHS=this.cfg.getProperty(B.MONTHS_SHORT.key).concat(); +break; +case YAHOO.widget.Calendar.LONG:this.Locale.LOCALE_MONTHS=this.cfg.getProperty(B.MONTHS_LONG.key).concat(); +break +}break; +case B.LOCALE_WEEKDAYS.key:switch(G){case YAHOO.widget.Calendar.ONE_CHAR:this.Locale.LOCALE_WEEKDAYS=this.cfg.getProperty(B.WEEKDAYS_1CHAR.key).concat(); +break; +case YAHOO.widget.Calendar.SHORT:this.Locale.LOCALE_WEEKDAYS=this.cfg.getProperty(B.WEEKDAYS_SHORT.key).concat(); +break; +case YAHOO.widget.Calendar.MEDIUM:this.Locale.LOCALE_WEEKDAYS=this.cfg.getProperty(B.WEEKDAYS_MEDIUM.key).concat(); +break; +case YAHOO.widget.Calendar.LONG:this.Locale.LOCALE_WEEKDAYS=this.cfg.getProperty(B.WEEKDAYS_LONG.key).concat(); +break +}var F=this.cfg.getProperty(B.START_WEEKDAY.key); +if(F>0){for(var A=0; +A<F; +++A){this.Locale.LOCALE_WEEKDAYS.push(this.Locale.LOCALE_WEEKDAYS.shift()) +}}break +}},configNavigator:function(C,A,D){var E=A[0]; +if(YAHOO.widget.CalendarNavigator&&(E===true||YAHOO.lang.isObject(E))){if(!this.oNavigator){this.oNavigator=new YAHOO.widget.CalendarNavigator(this); +function B(){if(!this.pages){this.oNavigator.erase() +}}this.beforeRenderEvent.subscribe(B,this,true) +}}else{if(this.oNavigator){this.oNavigator.destroy(); +this.oNavigator=null +}}},initStyles:function(){var A=YAHOO.widget.Calendar._STYLES; +this.Style={CSS_ROW_HEADER:A.CSS_ROW_HEADER,CSS_ROW_FOOTER:A.CSS_ROW_FOOTER,CSS_CELL:A.CSS_CELL,CSS_CELL_SELECTOR:A.CSS_CELL_SELECTOR,CSS_CELL_SELECTED:A.CSS_CELL_SELECTED,CSS_CELL_SELECTABLE:A.CSS_CELL_SELECTABLE,CSS_CELL_RESTRICTED:A.CSS_CELL_RESTRICTED,CSS_CELL_TODAY:A.CSS_CELL_TODAY,CSS_CELL_OOM:A.CSS_CELL_OOM,CSS_CELL_OOB:A.CSS_CELL_OOB,CSS_HEADER:A.CSS_HEADER,CSS_HEADER_TEXT:A.CSS_HEADER_TEXT,CSS_BODY:A.CSS_BODY,CSS_WEEKDAY_CELL:A.CSS_WEEKDAY_CELL,CSS_WEEKDAY_ROW:A.CSS_WEEKDAY_ROW,CSS_FOOTER:A.CSS_FOOTER,CSS_CALENDAR:A.CSS_CALENDAR,CSS_SINGLE:A.CSS_SINGLE,CSS_CONTAINER:A.CSS_CONTAINER,CSS_NAV_LEFT:A.CSS_NAV_LEFT,CSS_NAV_RIGHT:A.CSS_NAV_RIGHT,CSS_NAV:A.CSS_NAV,CSS_CLOSE:A.CSS_CLOSE,CSS_CELL_TOP:A.CSS_CELL_TOP,CSS_CELL_LEFT:A.CSS_CELL_LEFT,CSS_CELL_RIGHT:A.CSS_CELL_RIGHT,CSS_CELL_BOTTOM:A.CSS_CELL_BOTTOM,CSS_CELL_HOVER:A.CSS_CELL_HOVER,CSS_CELL_HIGHLIGHT1:A.CSS_CELL_HIGHLIGHT1,CSS_CELL_HIGHLIGHT2:A.CSS_CELL_HIGHLIGHT2,CSS_CELL_HIGHLIGHT3:A.CSS_CELL_HIGHLIGHT3,CSS_CELL_HIGHLIGHT4:A.CSS_CELL_HIGHLIGHT4} +},buildMonthLabel:function(){var A=this.cfg.getProperty(YAHOO.widget.Calendar._DEFAULT_CONFIG.PAGEDATE.key); +var C=this.Locale.LOCALE_MONTHS[A.getMonth()]+this.Locale.MY_LABEL_MONTH_SUFFIX; +var B=A.getFullYear()+this.Locale.MY_LABEL_YEAR_SUFFIX; +if(this.Locale.MY_LABEL_MONTH_POSITION==2||this.Locale.MY_LABEL_YEAR_POSITION==1){return B+C +}else{return C+B +}},buildDayLabel:function(A){return A.getDate() +},createTitleBar:function(A){var B=YAHOO.util.Dom.getElementsByClassName(YAHOO.widget.CalendarGroup.CSS_2UPTITLE,"div",this.oDomContainer)[0]||document.createElement("div"); +B.className=YAHOO.widget.CalendarGroup.CSS_2UPTITLE; +B.innerHTML=A; +this.oDomContainer.insertBefore(B,this.oDomContainer.firstChild); +YAHOO.util.Dom.addClass(this.oDomContainer,"withtitle"); +return B +},removeTitleBar:function(){var A=YAHOO.util.Dom.getElementsByClassName(YAHOO.widget.CalendarGroup.CSS_2UPTITLE,"div",this.oDomContainer)[0]||null; +if(A){YAHOO.util.Event.purgeElement(A); +this.oDomContainer.removeChild(A) +}YAHOO.util.Dom.removeClass(this.oDomContainer,"withtitle") +},createCloseButton:function(){var D=YAHOO.util.Dom,A=YAHOO.util.Event,C=YAHOO.widget.CalendarGroup.CSS_2UPCLOSE,F="us/my/bn/x_d.gif"; +var E=D.getElementsByClassName("link-close","a",this.oDomContainer)[0]; +if(!E){E=document.createElement("a"); +A.addListener(E,"click",function(H,G){G.hide(); +A.preventDefault(H) +},this) +}E.href="#"; +E.className="link-close"; +if(YAHOO.widget.Calendar.IMG_ROOT!==null){var B=D.getElementsByClassName(C,"img",E)[0]||document.createElement("img"); +B.src=YAHOO.widget.Calendar.IMG_ROOT+F; +B.className=C; +E.appendChild(B) +}else{E.innerHTML='<span class="'+C+" "+this.Style.CSS_CLOSE+'"></span>' +}this.oDomContainer.appendChild(E); +return E +},removeCloseButton:function(){var A=YAHOO.util.Dom.getElementsByClassName("link-close","a",this.oDomContainer)[0]||null; +if(A){YAHOO.util.Event.purgeElement(A); +this.oDomContainer.removeChild(A) +}},renderHeader:function(E){var H=7; +var F="us/tr/callt.gif"; +var G="us/tr/calrt.gif"; +var M=YAHOO.widget.Calendar._DEFAULT_CONFIG; +if(this.cfg.getProperty(M.SHOW_WEEK_HEADER.key)){H+=1 +}if(this.cfg.getProperty(M.SHOW_WEEK_FOOTER.key)){H+=1 +}E[E.length]="<thead>"; +E[E.length]="<tr>"; +E[E.length]='<th colspan="'+H+'" class="'+this.Style.CSS_HEADER_TEXT+'">'; +E[E.length]='<div class="'+this.Style.CSS_HEADER+'">'; +var K,L=false; +if(this.parent){if(this.index===0){K=true +}if(this.index==(this.parent.cfg.getProperty("pages")-1)){L=true +}}else{K=true; +L=true +}if(K){var A=this.cfg.getProperty(M.NAV_ARROW_LEFT.key); +if(A===null&&YAHOO.widget.Calendar.IMG_ROOT!==null){A=YAHOO.widget.Calendar.IMG_ROOT+F +}var C=(A===null)?"":' style="background-image:url('+A+')"'; +E[E.length]='<a class="'+this.Style.CSS_NAV_LEFT+'"'+C+" > </a>" +}var J=this.buildMonthLabel(); +var B=this.parent||this; +if(B.cfg.getProperty("navigator")){J='<a class="'+this.Style.CSS_NAV+'" href="#">'+J+"</a>" +}E[E.length]=J; +if(L){var D=this.cfg.getProperty(M.NAV_ARROW_RIGHT.key); +if(D===null&&YAHOO.widget.Calendar.IMG_ROOT!==null){D=YAHOO.widget.Calendar.IMG_ROOT+G +}var I=(D===null)?"":' style="background-image:url('+D+')"'; +E[E.length]='<a class="'+this.Style.CSS_NAV_RIGHT+'"'+I+" > </a>" +}E[E.length]="</div>\n</th>\n</tr>"; +if(this.cfg.getProperty(M.SHOW_WEEKDAYS.key)){E=this.buildWeekdays(E) +}E[E.length]="</thead>"; +return E +},buildWeekdays:function(C){var A=YAHOO.widget.Calendar._DEFAULT_CONFIG; +C[C.length]='<tr class="'+this.Style.CSS_WEEKDAY_ROW+'">'; +if(this.cfg.getProperty(A.SHOW_WEEK_HEADER.key)){C[C.length]="<th> </th>" +}for(var B=0; +B<this.Locale.LOCALE_WEEKDAYS.length; +++B){C[C.length]='<th class="calweekdaycell">'+this.Locale.LOCALE_WEEKDAYS[B]+"</th>" +}if(this.cfg.getProperty(A.SHOW_WEEK_FOOTER.key)){C[C.length]="<th> </th>" +}C[C.length]="</tr>"; +return C +},renderBody:function(c,a){var m=YAHOO.widget.Calendar._DEFAULT_CONFIG; +var AB=this.cfg.getProperty(m.START_WEEKDAY.key); +this.preMonthDays=c.getDay(); +if(AB>0){this.preMonthDays-=AB +}if(this.preMonthDays<0){this.preMonthDays+=7 +}this.monthDays=YAHOO.widget.DateMath.findMonthEnd(c).getDate(); +this.postMonthDays=YAHOO.widget.Calendar.DISPLAY_DAYS-this.preMonthDays-this.monthDays; +c=YAHOO.widget.DateMath.subtract(c,YAHOO.widget.DateMath.DAY,this.preMonthDays); +var Q,H; +var G="w"; +var W="_cell"; +var U="wd"; +var k="d"; +var I; +var h; +var O=this.today.getFullYear(); +var j=this.today.getMonth(); +var D=this.today.getDate(); +var q=this.cfg.getProperty(m.PAGEDATE.key); +var C=this.cfg.getProperty(m.HIDE_BLANK_WEEKS.key); +var Z=this.cfg.getProperty(m.SHOW_WEEK_FOOTER.key); +var T=this.cfg.getProperty(m.SHOW_WEEK_HEADER.key); +var M=this.cfg.getProperty(m.MINDATE.key); +var S=this.cfg.getProperty(m.MAXDATE.key); +if(M){M=YAHOO.widget.DateMath.clearTime(M) +}if(S){S=YAHOO.widget.DateMath.clearTime(S) +}a[a.length]='<tbody class="m'+(q.getMonth()+1)+" "+this.Style.CSS_BODY+'">'; +var z=0; +var J=document.createElement("div"); +var b=document.createElement("td"); +J.appendChild(b); +var o=this.parent||this; +for(var u=0; +u<6; +u++){Q=YAHOO.widget.DateMath.getWeekNumber(c,q.getFullYear(),AB); +H=G+Q; +if(u!==0&&C===true&&c.getMonth()!=q.getMonth()){break +}else{a[a.length]='<tr class="'+H+'">'; +if(T){a=this.renderRowHeader(Q,a) +}for(var AA=0; +AA<7; +AA++){I=[]; +this.clearElement(b); +b.className=this.Style.CSS_CELL; +b.id=this.id+W+z; +if(c.getDate()==D&&c.getMonth()==j&&c.getFullYear()==O){I[I.length]=o.renderCellStyleToday +}var R=[c.getFullYear(),c.getMonth()+1,c.getDate()]; +this.cellDates[this.cellDates.length]=R; +if(c.getMonth()!=q.getMonth()){I[I.length]=o.renderCellNotThisMonth +}else{YAHOO.util.Dom.addClass(b,U+c.getDay()); +YAHOO.util.Dom.addClass(b,k+c.getDate()); +for(var t=0; +t<this.renderStack.length; +++t){h=null; +var l=this.renderStack[t]; +var AC=l[0]; +var B; +var V; +var F; +switch(AC){case YAHOO.widget.Calendar.DATE:B=l[1][1]; +V=l[1][2]; +F=l[1][0]; +if(c.getMonth()+1==B&&c.getDate()==V&&c.getFullYear()==F){h=l[2]; +this.renderStack.splice(t,1) +}break; +case YAHOO.widget.Calendar.MONTH_DAY:B=l[1][0]; +V=l[1][1]; +if(c.getMonth()+1==B&&c.getDate()==V){h=l[2]; +this.renderStack.splice(t,1) +}break; +case YAHOO.widget.Calendar.RANGE:var Y=l[1][0]; +var X=l[1][1]; +var e=Y[1]; +var L=Y[2]; +var P=Y[0]; +var y=YAHOO.widget.DateMath.getDate(P,e-1,L); +var E=X[1]; +var g=X[2]; +var A=X[0]; +var w=YAHOO.widget.DateMath.getDate(A,E-1,g); +if(c.getTime()>=y.getTime()&&c.getTime()<=w.getTime()){h=l[2]; +if(c.getTime()==w.getTime()){this.renderStack.splice(t,1) +}}break; +case YAHOO.widget.Calendar.WEEKDAY:var K=l[1][0]; +if(c.getDay()+1==K){h=l[2] +}break; +case YAHOO.widget.Calendar.MONTH:B=l[1][0]; +if(c.getMonth()+1==B){h=l[2] +}break +}if(h){I[I.length]=h +}}}if(this._indexOfSelectedFieldArray(R)>-1){I[I.length]=o.renderCellStyleSelected +}if((M&&(c.getTime()<M.getTime()))||(S&&(c.getTime()>S.getTime()))){I[I.length]=o.renderOutOfBoundsDate +}else{I[I.length]=o.styleCellDefault; +I[I.length]=o.renderCellDefault +}for(var n=0; +n<I.length; +++n){if(I[n].call(o,c,b)==YAHOO.widget.Calendar.STOP_RENDER){break +}}c.setTime(c.getTime()+YAHOO.widget.DateMath.ONE_DAY_MS); +if(z>=0&&z<=6){YAHOO.util.Dom.addClass(b,this.Style.CSS_CELL_TOP) +}if((z%7)===0){YAHOO.util.Dom.addClass(b,this.Style.CSS_CELL_LEFT) +}if(((z+1)%7)===0){YAHOO.util.Dom.addClass(b,this.Style.CSS_CELL_RIGHT) +}var f=this.postMonthDays; +if(C&&f>=7){var N=Math.floor(f/7); +for(var v=0; +v<N; +++v){f-=7 +}}if(z>=((this.preMonthDays+f+this.monthDays)-7)){YAHOO.util.Dom.addClass(b,this.Style.CSS_CELL_BOTTOM) +}a[a.length]=J.innerHTML; +z++ +}if(Z){a=this.renderRowFooter(Q,a) +}a[a.length]="</tr>" +}}a[a.length]="</tbody>"; +return a +},renderFooter:function(A){return A +},render:function(){this.beforeRenderEvent.fire(); +var A=YAHOO.widget.Calendar._DEFAULT_CONFIG; +var C=YAHOO.widget.DateMath.findMonthStart(this.cfg.getProperty(A.PAGEDATE.key)); +this.resetRenderers(); +this.cellDates.length=0; +YAHOO.util.Event.purgeElement(this.oDomContainer,true); +var B=[]; +B[B.length]='<table cellSpacing="0" class="'+this.Style.CSS_CALENDAR+" y"+C.getFullYear()+'" id="'+this.id+'">'; +B=this.renderHeader(B); +B=this.renderBody(C,B); +B=this.renderFooter(B); +B[B.length]="</table>"; +this.oDomContainer.innerHTML=B.join("\n"); +this.applyListeners(); +this.cells=this.oDomContainer.getElementsByTagName("td"); +this.cfg.refireEvent(A.TITLE.key); +this.cfg.refireEvent(A.CLOSE.key); +this.cfg.refireEvent(A.IFRAME.key); +this.renderEvent.fire() +},applyListeners:function(){var K=this.oDomContainer; +var B=this.parent||this; +var G="a"; +var D="mousedown"; +var H=YAHOO.util.Dom.getElementsByClassName(this.Style.CSS_NAV_LEFT,G,K); +var C=YAHOO.util.Dom.getElementsByClassName(this.Style.CSS_NAV_RIGHT,G,K); +if(H&&H.length>0){this.linkLeft=H[0]; +YAHOO.util.Event.addListener(this.linkLeft,D,B.previousMonth,B,true) +}if(C&&C.length>0){this.linkRight=C[0]; +YAHOO.util.Event.addListener(this.linkRight,D,B.nextMonth,B,true) +}if(B.cfg.getProperty("navigator")!==null){this.applyNavListeners() +}if(this.domEventMap){var E,A; +for(var M in this.domEventMap){if(YAHOO.lang.hasOwnProperty(this.domEventMap,M)){var I=this.domEventMap[M]; +if(!(I instanceof Array)){I=[I] +}for(var F=0; +F<I.length; +F++){var L=I[F]; +A=YAHOO.util.Dom.getElementsByClassName(M,L.tag,this.oDomContainer); +for(var J=0; +J<A.length; +J++){E=A[J]; +YAHOO.util.Event.addListener(E,L.event,L.handler,L.scope,L.correct) +}}}}}YAHOO.util.Event.addListener(this.oDomContainer,"click",this.doSelectCell,this); +YAHOO.util.Event.addListener(this.oDomContainer,"mouseover",this.doCellMouseOver,this); +YAHOO.util.Event.addListener(this.oDomContainer,"mouseout",this.doCellMouseOut,this) +},applyNavListeners:function(){var D=YAHOO.util.Event; +var C=this.parent||this; +var F=this; +var B=YAHOO.util.Dom.getElementsByClassName(this.Style.CSS_NAV,"a",this.oDomContainer); +if(B.length>0){function A(J,I){var H=D.getTarget(J); +if(this===H||YAHOO.util.Dom.isAncestor(this,H)){D.preventDefault(J) +}var E=C.oNavigator; +if(E){var G=F.cfg.getProperty("pagedate"); +E.setYear(G.getFullYear()); +E.setMonth(G.getMonth()); +E.show() +}}D.addListener(B,"click",A) +}},getDateByCellId:function(B){var A=this.getDateFieldsByCellId(B); +return YAHOO.widget.DateMath.getDate(A[0],A[1]-1,A[2]) +},getDateFieldsByCellId:function(A){A=A.toLowerCase().split("_cell")[1]; +A=parseInt(A,10); +return this.cellDates[A] +},getCellIndex:function(C){var B=-1; +if(C){var A=C.getMonth(),H=C.getFullYear(),G=C.getDate(),E=this.cellDates; +for(var D=0; +D<E.length; +++D){var F=E[D]; +if(F[0]===H&&F[1]===A+1&&F[2]===G){B=D; +break +}}}return B +},renderOutOfBoundsDate:function(B,A){YAHOO.util.Dom.addClass(A,this.Style.CSS_CELL_OOB); +A.innerHTML=B.getDate(); +return YAHOO.widget.Calendar.STOP_RENDER +},renderRowHeader:function(B,A){A[A.length]='<th class="calrowhead">'+B+"</th>"; +return A +},renderRowFooter:function(B,A){A[A.length]='<th class="calrowfoot">'+B+"</th>"; +return A +},renderCellDefault:function(B,A){A.innerHTML='<a href="#" class="'+this.Style.CSS_CELL_SELECTOR+'">'+this.buildDayLabel(B)+"</a>" +},styleCellDefault:function(B,A){YAHOO.util.Dom.addClass(A,this.Style.CSS_CELL_SELECTABLE) +},renderCellStyleHighlight1:function(B,A){YAHOO.util.Dom.addClass(A,this.Style.CSS_CELL_HIGHLIGHT1) +},renderCellStyleHighlight2:function(B,A){YAHOO.util.Dom.addClass(A,this.Style.CSS_CELL_HIGHLIGHT2) +},renderCellStyleHighlight3:function(B,A){YAHOO.util.Dom.addClass(A,this.Style.CSS_CELL_HIGHLIGHT3) +},renderCellStyleHighlight4:function(B,A){YAHOO.util.Dom.addClass(A,this.Style.CSS_CELL_HIGHLIGHT4) +},renderCellStyleToday:function(B,A){YAHOO.util.Dom.addClass(A,this.Style.CSS_CELL_TODAY) +},renderCellStyleSelected:function(B,A){YAHOO.util.Dom.addClass(A,this.Style.CSS_CELL_SELECTED) +},renderCellNotThisMonth:function(B,A){YAHOO.util.Dom.addClass(A,this.Style.CSS_CELL_OOM); +A.innerHTML=B.getDate(); +return YAHOO.widget.Calendar.STOP_RENDER +},renderBodyCellRestricted:function(B,A){YAHOO.util.Dom.addClass(A,this.Style.CSS_CELL); +YAHOO.util.Dom.addClass(A,this.Style.CSS_CELL_RESTRICTED); +A.innerHTML=B.getDate(); +return YAHOO.widget.Calendar.STOP_RENDER +},addMonths:function(B){var A=YAHOO.widget.Calendar._DEFAULT_CONFIG.PAGEDATE.key; +this.cfg.setProperty(A,YAHOO.widget.DateMath.add(this.cfg.getProperty(A),YAHOO.widget.DateMath.MONTH,B)); +this.resetRenderers(); +this.changePageEvent.fire() +},subtractMonths:function(B){var A=YAHOO.widget.Calendar._DEFAULT_CONFIG.PAGEDATE.key; +this.cfg.setProperty(A,YAHOO.widget.DateMath.subtract(this.cfg.getProperty(A),YAHOO.widget.DateMath.MONTH,B)); +this.resetRenderers(); +this.changePageEvent.fire() +},addYears:function(B){var A=YAHOO.widget.Calendar._DEFAULT_CONFIG.PAGEDATE.key; +this.cfg.setProperty(A,YAHOO.widget.DateMath.add(this.cfg.getProperty(A),YAHOO.widget.DateMath.YEAR,B)); +this.resetRenderers(); +this.changePageEvent.fire() +},subtractYears:function(B){var A=YAHOO.widget.Calendar._DEFAULT_CONFIG.PAGEDATE.key; +this.cfg.setProperty(A,YAHOO.widget.DateMath.subtract(this.cfg.getProperty(A),YAHOO.widget.DateMath.YEAR,B)); +this.resetRenderers(); +this.changePageEvent.fire() +},nextMonth:function(){this.addMonths(1) +},previousMonth:function(){this.subtractMonths(1) +},nextYear:function(){this.addYears(1) +},previousYear:function(){this.subtractYears(1) +},reset:function(){var A=YAHOO.widget.Calendar._DEFAULT_CONFIG; +this.cfg.resetProperty(A.SELECTED.key); +this.cfg.resetProperty(A.PAGEDATE.key); +this.resetEvent.fire() +},clear:function(){var A=YAHOO.widget.Calendar._DEFAULT_CONFIG; +this.cfg.setProperty(A.SELECTED.key,[]); +this.cfg.setProperty(A.PAGEDATE.key,new Date(this.today.getTime())); +this.clearEvent.fire() +},select:function(C){var F=this._toFieldArray(C); +var B=[]; +var E=[]; +var G=YAHOO.widget.Calendar._DEFAULT_CONFIG.SELECTED.key; +for(var A=0; +A<F.length; +++A){var D=F[A]; +if(!this.isDateOOB(this._toDate(D))){if(B.length===0){this.beforeSelectEvent.fire(); +E=this.cfg.getProperty(G) +}B.push(D); +if(this._indexOfSelectedFieldArray(D)==-1){E[E.length]=D +}}}if(B.length>0){if(this.parent){this.parent.cfg.setProperty(G,E) +}else{this.cfg.setProperty(G,E) +}this.selectEvent.fire(B) +}return this.getSelectedDates() +},selectCell:function(D){var B=this.cells[D]; +var H=this.cellDates[D]; +var G=this._toDate(H); +var C=YAHOO.util.Dom.hasClass(B,this.Style.CSS_CELL_SELECTABLE); +if(C){this.beforeSelectEvent.fire(); +var F=YAHOO.widget.Calendar._DEFAULT_CONFIG.SELECTED.key; +var E=this.cfg.getProperty(F); +var A=H.concat(); +if(this._indexOfSelectedFieldArray(A)==-1){E[E.length]=A +}if(this.parent){this.parent.cfg.setProperty(F,E) +}else{this.cfg.setProperty(F,E) +}this.renderCellStyleSelected(G,B); +this.selectEvent.fire([A]); +this.doCellMouseOut.call(B,null,this) +}return this.getSelectedDates() +},deselect:function(E){var A=this._toFieldArray(E); +var D=[]; +var G=[]; +var H=YAHOO.widget.Calendar._DEFAULT_CONFIG.SELECTED.key; +for(var B=0; +B<A.length; +++B){var F=A[B]; +if(!this.isDateOOB(this._toDate(F))){if(D.length===0){this.beforeDeselectEvent.fire(); +G=this.cfg.getProperty(H) +}D.push(F); +var C=this._indexOfSelectedFieldArray(F); +if(C!=-1){G.splice(C,1) +}}}if(D.length>0){if(this.parent){this.parent.cfg.setProperty(H,G) +}else{this.cfg.setProperty(H,G) +}this.deselectEvent.fire(D) +}return this.getSelectedDates() +},deselectCell:function(E){var H=this.cells[E]; +var B=this.cellDates[E]; +var F=this._indexOfSelectedFieldArray(B); +var G=YAHOO.util.Dom.hasClass(H,this.Style.CSS_CELL_SELECTABLE); +if(G){this.beforeDeselectEvent.fire(); +var I=YAHOO.widget.Calendar._DEFAULT_CONFIG; +var D=this.cfg.getProperty(I.SELECTED.key); +var C=this._toDate(B); +var A=B.concat(); +if(F>-1){if(this.cfg.getProperty(I.PAGEDATE.key).getMonth()==C.getMonth()&&this.cfg.getProperty(I.PAGEDATE.key).getFullYear()==C.getFullYear()){YAHOO.util.Dom.removeClass(H,this.Style.CSS_CELL_SELECTED) +}D.splice(F,1) +}if(this.parent){this.parent.cfg.setProperty(I.SELECTED.key,D) +}else{this.cfg.setProperty(I.SELECTED.key,D) +}this.deselectEvent.fire(A) +}return this.getSelectedDates() +},deselectAll:function(){this.beforeDeselectEvent.fire(); +var D=YAHOO.widget.Calendar._DEFAULT_CONFIG.SELECTED.key; +var A=this.cfg.getProperty(D); +var B=A.length; +var C=A.concat(); +if(this.parent){this.parent.cfg.setProperty(D,[]) +}else{this.cfg.setProperty(D,[]) +}if(B>0){this.deselectEvent.fire(C) +}return this.getSelectedDates() +},_toFieldArray:function(B){var A=[]; +if(B instanceof Date){A=[[B.getFullYear(),B.getMonth()+1,B.getDate()]] +}else{if(YAHOO.lang.isString(B)){A=this._parseDates(B) +}else{if(YAHOO.lang.isArray(B)){for(var C=0; +C<B.length; +++C){var D=B[C]; +A[A.length]=[D.getFullYear(),D.getMonth()+1,D.getDate()] +}}}}return A +},toDate:function(A){return this._toDate(A) +},_toDate:function(A){if(A instanceof Date){return A +}else{return YAHOO.widget.DateMath.getDate(A[0],A[1]-1,A[2]) +}},_fieldArraysAreEqual:function(C,B){var A=false; +if(C[0]==B[0]&&C[1]==B[1]&&C[2]==B[2]){A=true +}return A +},_indexOfSelectedFieldArray:function(E){var D=-1; +var A=this.cfg.getProperty(YAHOO.widget.Calendar._DEFAULT_CONFIG.SELECTED.key); +for(var C=0; +C<A.length; +++C){var B=A[C]; +if(E[0]==B[0]&&E[1]==B[1]&&E[2]==B[2]){D=C; +break +}}return D +},isDateOOM:function(A){return(A.getMonth()!=this.cfg.getProperty(YAHOO.widget.Calendar._DEFAULT_CONFIG.PAGEDATE.key).getMonth()) +},isDateOOB:function(D){var A=YAHOO.widget.Calendar._DEFAULT_CONFIG; +var E=this.cfg.getProperty(A.MINDATE.key); +var F=this.cfg.getProperty(A.MAXDATE.key); +var C=YAHOO.widget.DateMath; +if(E){E=C.clearTime(E) +}if(F){F=C.clearTime(F) +}var B=new Date(D.getTime()); +B=C.clearTime(B); +return((E&&B.getTime()<E.getTime())||(F&&B.getTime()>F.getTime())) +},_parsePageDate:function(B){var E; +var A=YAHOO.widget.Calendar._DEFAULT_CONFIG; +if(B){if(B instanceof Date){E=YAHOO.widget.DateMath.findMonthStart(B) +}else{var F,D,C; +C=B.split(this.cfg.getProperty(A.DATE_FIELD_DELIMITER.key)); +F=parseInt(C[this.cfg.getProperty(A.MY_MONTH_POSITION.key)-1],10)-1; +D=parseInt(C[this.cfg.getProperty(A.MY_YEAR_POSITION.key)-1],10); +E=YAHOO.widget.DateMath.getDate(D,F,1) +}}else{E=YAHOO.widget.DateMath.getDate(this.today.getFullYear(),this.today.getMonth(),1) +}return E +},onBeforeSelect:function(){if(this.cfg.getProperty(YAHOO.widget.Calendar._DEFAULT_CONFIG.MULTI_SELECT.key)===false){if(this.parent){this.parent.callChildFunction("clearAllBodyCellStyles",this.Style.CSS_CELL_SELECTED); +this.parent.deselectAll() +}else{this.clearAllBodyCellStyles(this.Style.CSS_CELL_SELECTED); +this.deselectAll() +}}},onSelect:function(A){},onBeforeDeselect:function(){},onDeselect:function(A){},onChangePage:function(){this.render() +},onRender:function(){},onReset:function(){this.render() +},onClear:function(){this.render() +},validate:function(){return true +},_parseDate:function(C){var D=C.split(this.Locale.DATE_FIELD_DELIMITER); +var A; +if(D.length==2){A=[D[this.Locale.MD_MONTH_POSITION-1],D[this.Locale.MD_DAY_POSITION-1]]; +A.type=YAHOO.widget.Calendar.MONTH_DAY +}else{A=[D[this.Locale.MDY_YEAR_POSITION-1],D[this.Locale.MDY_MONTH_POSITION-1],D[this.Locale.MDY_DAY_POSITION-1]]; +A.type=YAHOO.widget.Calendar.DATE +}for(var B=0; +B<A.length; +B++){A[B]=parseInt(A[B],10) +}return A +},_parseDates:function(B){var I=[]; +var H=B.split(this.Locale.DATE_DELIMITER); +for(var G=0; +G<H.length; +++G){var F=H[G]; +if(F.indexOf(this.Locale.DATE_RANGE_DELIMITER)!=-1){var A=F.split(this.Locale.DATE_RANGE_DELIMITER); +var E=this._parseDate(A[0]); +var J=this._parseDate(A[1]); +var D=this._parseRange(E,J); +I=I.concat(D) +}else{var C=this._parseDate(F); +I.push(C) +}}return I +},_parseRange:function(A,E){var B=YAHOO.widget.DateMath.add(YAHOO.widget.DateMath.getDate(A[0],A[1]-1,A[2]),YAHOO.widget.DateMath.DAY,1); +var D=YAHOO.widget.DateMath.getDate(E[0],E[1]-1,E[2]); +var C=[]; +C.push(A); +while(B.getTime()<=D.getTime()){C.push([B.getFullYear(),B.getMonth()+1,B.getDate()]); +B=YAHOO.widget.DateMath.add(B,YAHOO.widget.DateMath.DAY,1) +}return C +},resetRenderers:function(){this.renderStack=this._renderStack.concat() +},removeRenderers:function(){this._renderStack=[]; +this.renderStack=[] +},clearElement:function(A){A.innerHTML=" "; +A.className="" +},addRenderer:function(A,B){var D=this._parseDates(A); +for(var C=0; +C<D.length; +++C){var E=D[C]; +if(E.length==2){if(E[0] instanceof Array){this._addRenderer(YAHOO.widget.Calendar.RANGE,E,B) +}else{this._addRenderer(YAHOO.widget.Calendar.MONTH_DAY,E,B) +}}else{if(E.length==3){this._addRenderer(YAHOO.widget.Calendar.DATE,E,B) +}}}},_addRenderer:function(B,C,A){var D=[B,C,A]; +this.renderStack.unshift(D); +this._renderStack=this.renderStack.concat() +},addMonthRenderer:function(B,A){this._addRenderer(YAHOO.widget.Calendar.MONTH,[B],A) +},addWeekdayRenderer:function(B,A){this._addRenderer(YAHOO.widget.Calendar.WEEKDAY,[B],A) +},clearAllBodyCellStyles:function(A){for(var B=0; +B<this.cells.length; +++B){YAHOO.util.Dom.removeClass(this.cells[B],A) +}},setMonth:function(C){var A=YAHOO.widget.Calendar._DEFAULT_CONFIG.PAGEDATE.key; +var B=this.cfg.getProperty(A); +B.setMonth(parseInt(C,10)); +this.cfg.setProperty(A,B) +},setYear:function(B){var A=YAHOO.widget.Calendar._DEFAULT_CONFIG.PAGEDATE.key; +var C=this.cfg.getProperty(A); +C.setFullYear(parseInt(B,10)); +this.cfg.setProperty(A,C) +},getSelectedDates:function(){var C=[]; +var B=this.cfg.getProperty(YAHOO.widget.Calendar._DEFAULT_CONFIG.SELECTED.key); +for(var E=0; +E<B.length; +++E){var D=B[E]; +var A=YAHOO.widget.DateMath.getDate(D[0],D[1]-1,D[2]); +C.push(A) +}C.sort(function(G,F){return G-F +}); +return C +},hide:function(){if(this.beforeHideEvent.fire()){this.oDomContainer.style.display="none"; +this.hideEvent.fire() +}},show:function(){if(this.beforeShowEvent.fire()){this.oDomContainer.style.display="block"; +this.showEvent.fire() +}},browser:(function(){var A=navigator.userAgent.toLowerCase(); +if(A.indexOf("opera")!=-1){return"opera" +}else{if(A.indexOf("msie 7")!=-1){return"ie7" +}else{if(A.indexOf("msie")!=-1){return"ie" +}else{if(A.indexOf("safari")!=-1){return"safari" +}else{if(A.indexOf("gecko")!=-1){return"gecko" +}else{return false +}}}}}})(),toString:function(){return"Calendar "+this.id +}}; +YAHOO.widget.Calendar_Core=YAHOO.widget.Calendar; +YAHOO.widget.Cal_Core=YAHOO.widget.Calendar; +YAHOO.widget.CalendarGroup=function(C,A,B){if(arguments.length>0){this.init.apply(this,arguments) +}}; +YAHOO.widget.CalendarGroup.prototype={init:function(D,B,C){var A=this._parseArgs(arguments); +D=A.id; +B=A.container; +C=A.config; +this.oDomContainer=YAHOO.util.Dom.get(B); +if(!this.oDomContainer.id){this.oDomContainer.id=YAHOO.util.Dom.generateId() +}if(!D){D=this.oDomContainer.id+"_t" +}this.id=D; +this.containerId=this.oDomContainer.id; +this.initEvents(); +this.initStyles(); +this.pages=[]; +YAHOO.util.Dom.addClass(this.oDomContainer,YAHOO.widget.CalendarGroup.CSS_CONTAINER); +YAHOO.util.Dom.addClass(this.oDomContainer,YAHOO.widget.CalendarGroup.CSS_MULTI_UP); +this.cfg=new YAHOO.util.Config(this); +this.Options={}; +this.Locale={}; +this.setupConfig(); +if(C){this.cfg.applyConfig(C,true) +}this.cfg.fireQueue(); +if(YAHOO.env.ua.opera){this.renderEvent.subscribe(this._fixWidth,this,true); +this.showEvent.subscribe(this._fixWidth,this,true) +}},setupConfig:function(){var A=YAHOO.widget.CalendarGroup._DEFAULT_CONFIG; +this.cfg.addProperty(A.PAGES.key,{value:A.PAGES.value,validator:this.cfg.checkNumber,handler:this.configPages}); +this.cfg.addProperty(A.PAGEDATE.key,{value:new Date(),handler:this.configPageDate}); +this.cfg.addProperty(A.SELECTED.key,{value:[],handler:this.configSelected}); +this.cfg.addProperty(A.TITLE.key,{value:A.TITLE.value,handler:this.configTitle}); +this.cfg.addProperty(A.CLOSE.key,{value:A.CLOSE.value,handler:this.configClose}); +this.cfg.addProperty(A.IFRAME.key,{value:A.IFRAME.value,handler:this.configIframe,validator:this.cfg.checkBoolean}); +this.cfg.addProperty(A.MINDATE.key,{value:A.MINDATE.value,handler:this.delegateConfig}); +this.cfg.addProperty(A.MAXDATE.key,{value:A.MAXDATE.value,handler:this.delegateConfig}); +this.cfg.addProperty(A.MULTI_SELECT.key,{value:A.MULTI_SELECT.value,handler:this.delegateConfig,validator:this.cfg.checkBoolean}); +this.cfg.addProperty(A.START_WEEKDAY.key,{value:A.START_WEEKDAY.value,handler:this.delegateConfig,validator:this.cfg.checkNumber}); +this.cfg.addProperty(A.SHOW_WEEKDAYS.key,{value:A.SHOW_WEEKDAYS.value,handler:this.delegateConfig,validator:this.cfg.checkBoolean}); +this.cfg.addProperty(A.SHOW_WEEK_HEADER.key,{value:A.SHOW_WEEK_HEADER.value,handler:this.delegateConfig,validator:this.cfg.checkBoolean}); +this.cfg.addProperty(A.SHOW_WEEK_FOOTER.key,{value:A.SHOW_WEEK_FOOTER.value,handler:this.delegateConfig,validator:this.cfg.checkBoolean}); +this.cfg.addProperty(A.HIDE_BLANK_WEEKS.key,{value:A.HIDE_BLANK_WEEKS.value,handler:this.delegateConfig,validator:this.cfg.checkBoolean}); +this.cfg.addProperty(A.NAV_ARROW_LEFT.key,{value:A.NAV_ARROW_LEFT.value,handler:this.delegateConfig}); +this.cfg.addProperty(A.NAV_ARROW_RIGHT.key,{value:A.NAV_ARROW_RIGHT.value,handler:this.delegateConfig}); +this.cfg.addProperty(A.MONTHS_SHORT.key,{value:A.MONTHS_SHORT.value,handler:this.delegateConfig}); +this.cfg.addProperty(A.MONTHS_LONG.key,{value:A.MONTHS_LONG.value,handler:this.delegateConfig}); +this.cfg.addProperty(A.WEEKDAYS_1CHAR.key,{value:A.WEEKDAYS_1CHAR.value,handler:this.delegateConfig}); +this.cfg.addProperty(A.WEEKDAYS_SHORT.key,{value:A.WEEKDAYS_SHORT.value,handler:this.delegateConfig}); +this.cfg.addProperty(A.WEEKDAYS_MEDIUM.key,{value:A.WEEKDAYS_MEDIUM.value,handler:this.delegateConfig}); +this.cfg.addProperty(A.WEEKDAYS_LONG.key,{value:A.WEEKDAYS_LONG.value,handler:this.delegateConfig}); +this.cfg.addProperty(A.LOCALE_MONTHS.key,{value:A.LOCALE_MONTHS.value,handler:this.delegateConfig}); +this.cfg.addProperty(A.LOCALE_WEEKDAYS.key,{value:A.LOCALE_WEEKDAYS.value,handler:this.delegateConfig}); +this.cfg.addProperty(A.DATE_DELIMITER.key,{value:A.DATE_DELIMITER.value,handler:this.delegateConfig}); +this.cfg.addProperty(A.DATE_FIELD_DELIMITER.key,{value:A.DATE_FIELD_DELIMITER.value,handler:this.delegateConfig}); +this.cfg.addProperty(A.DATE_RANGE_DELIMITER.key,{value:A.DATE_RANGE_DELIMITER.value,handler:this.delegateConfig}); +this.cfg.addProperty(A.MY_MONTH_POSITION.key,{value:A.MY_MONTH_POSITION.value,handler:this.delegateConfig,validator:this.cfg.checkNumber}); +this.cfg.addProperty(A.MY_YEAR_POSITION.key,{value:A.MY_YEAR_POSITION.value,handler:this.delegateConfig,validator:this.cfg.checkNumber}); +this.cfg.addProperty(A.MD_MONTH_POSITION.key,{value:A.MD_MONTH_POSITION.value,handler:this.delegateConfig,validator:this.cfg.checkNumber}); +this.cfg.addProperty(A.MD_DAY_POSITION.key,{value:A.MD_DAY_POSITION.value,handler:this.delegateConfig,validator:this.cfg.checkNumber}); +this.cfg.addProperty(A.MDY_MONTH_POSITION.key,{value:A.MDY_MONTH_POSITION.value,handler:this.delegateConfig,validator:this.cfg.checkNumber}); +this.cfg.addProperty(A.MDY_DAY_POSITION.key,{value:A.MDY_DAY_POSITION.value,handler:this.delegateConfig,validator:this.cfg.checkNumber}); +this.cfg.addProperty(A.MDY_YEAR_POSITION.key,{value:A.MDY_YEAR_POSITION.value,handler:this.delegateConfig,validator:this.cfg.checkNumber}); +this.cfg.addProperty(A.MY_LABEL_MONTH_POSITION.key,{value:A.MY_LABEL_MONTH_POSITION.value,handler:this.delegateConfig,validator:this.cfg.checkNumber}); +this.cfg.addProperty(A.MY_LABEL_YEAR_POSITION.key,{value:A.MY_LABEL_YEAR_POSITION.value,handler:this.delegateConfig,validator:this.cfg.checkNumber}); +this.cfg.addProperty(A.MY_LABEL_MONTH_SUFFIX.key,{value:A.MY_LABEL_MONTH_SUFFIX.value,handler:this.delegateConfig}); +this.cfg.addProperty(A.MY_LABEL_YEAR_SUFFIX.key,{value:A.MY_LABEL_YEAR_SUFFIX.value,handler:this.delegateConfig}); +this.cfg.addProperty(A.NAV.key,{value:A.NAV.value,handler:this.configNavigator}) +},initEvents:function(){var C=this; +var E="Event"; +var B=function(G,J,F){for(var I=0; +I<C.pages.length; +++I){var H=C.pages[I]; +H[this.type+E].subscribe(G,J,F) +}}; +var A=function(F,I){for(var H=0; +H<C.pages.length; +++H){var G=C.pages[H]; +G[this.type+E].unsubscribe(F,I) +}}; +var D=YAHOO.widget.Calendar._EVENT_TYPES; +this.beforeSelectEvent=new YAHOO.util.CustomEvent(D.BEFORE_SELECT); +this.beforeSelectEvent.subscribe=B; +this.beforeSelectEvent.unsubscribe=A; +this.selectEvent=new YAHOO.util.CustomEvent(D.SELECT); +this.selectEvent.subscribe=B; +this.selectEvent.unsubscribe=A; +this.beforeDeselectEvent=new YAHOO.util.CustomEvent(D.BEFORE_DESELECT); +this.beforeDeselectEvent.subscribe=B; +this.beforeDeselectEvent.unsubscribe=A; +this.deselectEvent=new YAHOO.util.CustomEvent(D.DESELECT); +this.deselectEvent.subscribe=B; +this.deselectEvent.unsubscribe=A; +this.changePageEvent=new YAHOO.util.CustomEvent(D.CHANGE_PAGE); +this.changePageEvent.subscribe=B; +this.changePageEvent.unsubscribe=A; +this.beforeRenderEvent=new YAHOO.util.CustomEvent(D.BEFORE_RENDER); +this.beforeRenderEvent.subscribe=B; +this.beforeRenderEvent.unsubscribe=A; +this.renderEvent=new YAHOO.util.CustomEvent(D.RENDER); +this.renderEvent.subscribe=B; +this.renderEvent.unsubscribe=A; +this.resetEvent=new YAHOO.util.CustomEvent(D.RESET); +this.resetEvent.subscribe=B; +this.resetEvent.unsubscribe=A; +this.clearEvent=new YAHOO.util.CustomEvent(D.CLEAR); +this.clearEvent.subscribe=B; +this.clearEvent.unsubscribe=A; +this.beforeShowEvent=new YAHOO.util.CustomEvent(D.BEFORE_SHOW); +this.showEvent=new YAHOO.util.CustomEvent(D.SHOW); +this.beforeHideEvent=new YAHOO.util.CustomEvent(D.BEFORE_HIDE); +this.hideEvent=new YAHOO.util.CustomEvent(D.HIDE); +this.beforeShowNavEvent=new YAHOO.util.CustomEvent(D.BEFORE_SHOW_NAV); +this.showNavEvent=new YAHOO.util.CustomEvent(D.SHOW_NAV); +this.beforeHideNavEvent=new YAHOO.util.CustomEvent(D.BEFORE_HIDE_NAV); +this.hideNavEvent=new YAHOO.util.CustomEvent(D.HIDE_NAV); +this.beforeRenderNavEvent=new YAHOO.util.CustomEvent(D.BEFORE_RENDER_NAV); +this.renderNavEvent=new YAHOO.util.CustomEvent(D.RENDER_NAV) +},configPages:function(K,J,G){var E=J[0]; +var C=YAHOO.widget.CalendarGroup._DEFAULT_CONFIG.PAGEDATE.key; +var O="_"; +var L="groupcal"; +var N="first-of-type"; +var D="last-of-type"; +for(var B=0; +B<E; +++B){var M=this.id+O+B; +var I=this.containerId+O+B; +var H=this.cfg.getConfig(); +H.close=false; +H.title=false; +H.navigator=null; +var A=this.constructChild(M,I,H); +var F=A.cfg.getProperty(C); +this._setMonthOnDate(F,F.getMonth()+B); +A.cfg.setProperty(C,F); +YAHOO.util.Dom.removeClass(A.oDomContainer,this.Style.CSS_SINGLE); +YAHOO.util.Dom.addClass(A.oDomContainer,L); +if(B===0){YAHOO.util.Dom.addClass(A.oDomContainer,N) +}if(B==(E-1)){YAHOO.util.Dom.addClass(A.oDomContainer,D) +}A.parent=this; +A.index=B; +this.pages[this.pages.length]=A +}},configPageDate:function(H,G,E){var C=G[0]; +var F; +var D=YAHOO.widget.CalendarGroup._DEFAULT_CONFIG.PAGEDATE.key; +for(var B=0; +B<this.pages.length; +++B){var A=this.pages[B]; +if(B===0){F=A._parsePageDate(C); +A.cfg.setProperty(D,F) +}else{var I=new Date(F); +this._setMonthOnDate(I,I.getMonth()+B); +A.cfg.setProperty(D,I) +}}},configSelected:function(C,A,E){var D=YAHOO.widget.CalendarGroup._DEFAULT_CONFIG.SELECTED.key; +this.delegateConfig(C,A,E); +var B=(this.pages.length>0)?this.pages[0].cfg.getProperty(D):[]; +this.cfg.setProperty(D,B,true) +},delegateConfig:function(B,A,E){var F=A[0]; +var D; +for(var C=0; +C<this.pages.length; +C++){D=this.pages[C]; +D.cfg.setProperty(B,F) +}},setChildFunction:function(D,B){var A=this.cfg.getProperty(YAHOO.widget.CalendarGroup._DEFAULT_CONFIG.PAGES.key); +for(var C=0; +C<A; +++C){this.pages[C][D]=B +}},callChildFunction:function(F,B){var A=this.cfg.getProperty(YAHOO.widget.CalendarGroup._DEFAULT_CONFIG.PAGES.key); +for(var E=0; +E<A; +++E){var D=this.pages[E]; +if(D[F]){var C=D[F]; +C.call(D,B) +}}},constructChild:function(D,B,C){var A=document.getElementById(B); +if(!A){A=document.createElement("div"); +A.id=B; +this.oDomContainer.appendChild(A) +}return new YAHOO.widget.Calendar(D,B,C) +},setMonth:function(E){E=parseInt(E,10); +var F; +var B=YAHOO.widget.CalendarGroup._DEFAULT_CONFIG.PAGEDATE.key; +for(var D=0; +D<this.pages.length; +++D){var C=this.pages[D]; +var A=C.cfg.getProperty(B); +if(D===0){F=A.getFullYear() +}else{A.setFullYear(F) +}this._setMonthOnDate(A,E+D); +C.cfg.setProperty(B,A) +}},setYear:function(C){var B=YAHOO.widget.CalendarGroup._DEFAULT_CONFIG.PAGEDATE.key; +C=parseInt(C,10); +for(var E=0; +E<this.pages.length; +++E){var D=this.pages[E]; +var A=D.cfg.getProperty(B); +if((A.getMonth()+1)==1&&E>0){C+=1 +}D.setYear(C) +}},render:function(){this.renderHeader(); +for(var B=0; +B<this.pages.length; +++B){var A=this.pages[B]; +A.render() +}this.renderFooter() +},select:function(A){for(var C=0; +C<this.pages.length; +++C){var B=this.pages[C]; +B.select(A) +}return this.getSelectedDates() +},selectCell:function(A){for(var C=0; +C<this.pages.length; +++C){var B=this.pages[C]; +B.selectCell(A) +}return this.getSelectedDates() +},deselect:function(A){for(var C=0; +C<this.pages.length; +++C){var B=this.pages[C]; +B.deselect(A) +}return this.getSelectedDates() +},deselectAll:function(){for(var B=0; +B<this.pages.length; +++B){var A=this.pages[B]; +A.deselectAll() +}return this.getSelectedDates() +},deselectCell:function(A){for(var C=0; +C<this.pages.length; +++C){var B=this.pages[C]; +B.deselectCell(A) +}return this.getSelectedDates() +},reset:function(){for(var B=0; +B<this.pages.length; +++B){var A=this.pages[B]; +A.reset() +}},clear:function(){for(var B=0; +B<this.pages.length; +++B){var A=this.pages[B]; +A.clear() +}},nextMonth:function(){for(var B=0; +B<this.pages.length; +++B){var A=this.pages[B]; +A.nextMonth() +}},previousMonth:function(){for(var B=this.pages.length-1; +B>=0; +--B){var A=this.pages[B]; +A.previousMonth() +}},nextYear:function(){for(var B=0; +B<this.pages.length; +++B){var A=this.pages[B]; +A.nextYear() +}},previousYear:function(){for(var B=0; +B<this.pages.length; +++B){var A=this.pages[B]; +A.previousYear() +}},getSelectedDates:function(){var C=[]; +var B=this.cfg.getProperty(YAHOO.widget.CalendarGroup._DEFAULT_CONFIG.SELECTED.key); +for(var E=0; +E<B.length; +++E){var D=B[E]; +var A=YAHOO.widget.DateMath.getDate(D[0],D[1]-1,D[2]); +C.push(A) +}C.sort(function(G,F){return G-F +}); +return C +},addRenderer:function(A,B){for(var D=0; +D<this.pages.length; +++D){var C=this.pages[D]; +C.addRenderer(A,B) +}},addMonthRenderer:function(D,A){for(var C=0; +C<this.pages.length; +++C){var B=this.pages[C]; +B.addMonthRenderer(D,A) +}},addWeekdayRenderer:function(B,A){for(var D=0; +D<this.pages.length; +++D){var C=this.pages[D]; +C.addWeekdayRenderer(B,A) +}},removeRenderers:function(){this.callChildFunction("removeRenderers") +},renderHeader:function(){},renderFooter:function(){},addMonths:function(A){this.callChildFunction("addMonths",A) +},subtractMonths:function(A){this.callChildFunction("subtractMonths",A) +},addYears:function(A){this.callChildFunction("addYears",A) +},subtractYears:function(A){this.callChildFunction("subtractYears",A) +},getCalendarPage:function(D){var F=null; +if(D){var G=D.getFullYear(),C=D.getMonth(); +var B=this.pages; +for(var E=0; +E<B.length; +++E){var A=B[E].cfg.getProperty("pagedate"); +if(A.getFullYear()===G&&A.getMonth()===C){F=B[E]; +break +}}}return F +},_setMonthOnDate:function(C,D){if(YAHOO.env.ua.webkit&&YAHOO.env.ua.webkit<420&&(D<0||D>11)){var B=YAHOO.widget.DateMath; +var A=B.add(C,B.MONTH,D-C.getMonth()); +C.setTime(A.getTime()) +}else{C.setMonth(D) +}},_fixWidth:function(){var A=0; +for(var C=0; +C<this.pages.length; +++C){var B=this.pages[C]; +A+=B.oDomContainer.offsetWidth +}if(A>0){this.oDomContainer.style.width=A+"px" +}},toString:function(){return"CalendarGroup "+this.id +}}; +YAHOO.widget.CalendarGroup.CSS_CONTAINER="yui-calcontainer"; +YAHOO.widget.CalendarGroup.CSS_MULTI_UP="multi"; +YAHOO.widget.CalendarGroup.CSS_2UPTITLE="title"; +YAHOO.widget.CalendarGroup.CSS_2UPCLOSE="close-icon"; +YAHOO.lang.augmentProto(YAHOO.widget.CalendarGroup,YAHOO.widget.Calendar,"buildDayLabel","buildMonthLabel","renderOutOfBoundsDate","renderRowHeader","renderRowFooter","renderCellDefault","styleCellDefault","renderCellStyleHighlight1","renderCellStyleHighlight2","renderCellStyleHighlight3","renderCellStyleHighlight4","renderCellStyleToday","renderCellStyleSelected","renderCellNotThisMonth","renderBodyCellRestricted","initStyles","configTitle","configClose","configIframe","configNavigator","createTitleBar","createCloseButton","removeTitleBar","removeCloseButton","hide","show","toDate","_parseArgs","browser"); +YAHOO.widget.CalendarGroup._DEFAULT_CONFIG=YAHOO.widget.Calendar._DEFAULT_CONFIG; +YAHOO.widget.CalendarGroup._DEFAULT_CONFIG.PAGES={key:"pages",value:2}; +YAHOO.widget.CalGrp=YAHOO.widget.CalendarGroup; +YAHOO.widget.Calendar2up=function(C,A,B){this.init(C,A,B) +}; +YAHOO.extend(YAHOO.widget.Calendar2up,YAHOO.widget.CalendarGroup); +YAHOO.widget.Cal2up=YAHOO.widget.Calendar2up; +YAHOO.widget.CalendarNavigator=function(A){this.init(A) +}; +(function(){var A=YAHOO.widget.CalendarNavigator; +A.CLASSES={NAV:"yui-cal-nav",NAV_VISIBLE:"yui-cal-nav-visible",MASK:"yui-cal-nav-mask",YEAR:"yui-cal-nav-y",MONTH:"yui-cal-nav-m",BUTTONS:"yui-cal-nav-b",BUTTON:"yui-cal-nav-btn",ERROR:"yui-cal-nav-e",YEAR_CTRL:"yui-cal-nav-yc",MONTH_CTRL:"yui-cal-nav-mc",INVALID:"yui-invalid",DEFAULT:"yui-default"}; +A._DEFAULT_CFG={strings:{month:"Month",year:"Year",submit:"Okay",cancel:"Cancel",invalidYear:"Year needs to be a number"},monthFormat:YAHOO.widget.Calendar.LONG,initialFocus:"year"}; +A.ID_SUFFIX="_nav"; +A.MONTH_SUFFIX="_month"; +A.YEAR_SUFFIX="_year"; +A.ERROR_SUFFIX="_error"; +A.CANCEL_SUFFIX="_cancel"; +A.SUBMIT_SUFFIX="_submit"; +A.YR_MAX_DIGITS=4; +A.YR_MINOR_INC=1; +A.YR_MAJOR_INC=10; +A.UPDATE_DELAY=50; +A.YR_PATTERN=/^\d+$/; +A.TRIM=/^\s*(.*?)\s*$/ +})(); +YAHOO.widget.CalendarNavigator.prototype={id:null,cal:null,navEl:null,maskEl:null,yearEl:null,monthEl:null,errorEl:null,submitEl:null,cancelEl:null,firstCtrl:null,lastCtrl:null,_doc:null,_year:null,_month:0,__rendered:false,init:function(A){var C=A.oDomContainer; +this.cal=A; +this.id=C.id+YAHOO.widget.CalendarNavigator.ID_SUFFIX; +this._doc=C.ownerDocument; +var B=YAHOO.env.ua.ie; +this.__isIEQuirks=(B&&((B<=6)||(B===7&&this._doc.compatMode=="BackCompat"))) +},show:function(){var A=YAHOO.widget.CalendarNavigator.CLASSES; +if(this.cal.beforeShowNavEvent.fire()){if(!this.__rendered){this.render() +}this.clearErrors(); +this._updateMonthUI(); +this._updateYearUI(); +this._show(this.navEl,true); +this.setInitialFocus(); +this.showMask(); +YAHOO.util.Dom.addClass(this.cal.oDomContainer,A.NAV_VISIBLE); +this.cal.showNavEvent.fire() +}},hide:function(){var A=YAHOO.widget.CalendarNavigator.CLASSES; +if(this.cal.beforeHideNavEvent.fire()){this._show(this.navEl,false); +this.hideMask(); +YAHOO.util.Dom.removeClass(this.cal.oDomContainer,A.NAV_VISIBLE); +this.cal.hideNavEvent.fire() +}},showMask:function(){this._show(this.maskEl,true); +if(this.__isIEQuirks){this._syncMask() +}},hideMask:function(){this._show(this.maskEl,false) +},getMonth:function(){return this._month +},getYear:function(){return this._year +},setMonth:function(A){if(A>=0&&A<12){this._month=A +}this._updateMonthUI() +},setYear:function(B){var A=YAHOO.widget.CalendarNavigator.YR_PATTERN; +if(YAHOO.lang.isNumber(B)&&A.test(B+"")){this._year=B +}this._updateYearUI() +},render:function(){this.cal.beforeRenderNavEvent.fire(); +if(!this.__rendered){this.createNav(); +this.createMask(); +this.applyListeners(); +this.__rendered=true +}this.cal.renderNavEvent.fire() +},createNav:function(){var B=YAHOO.widget.CalendarNavigator; +var C=this._doc; +var D=C.createElement("div"); +D.className=B.CLASSES.NAV; +var A=this.renderNavContents([]); +D.innerHTML=A.join(""); +this.cal.oDomContainer.appendChild(D); +this.navEl=D; +this.yearEl=C.getElementById(this.id+B.YEAR_SUFFIX); +this.monthEl=C.getElementById(this.id+B.MONTH_SUFFIX); +this.errorEl=C.getElementById(this.id+B.ERROR_SUFFIX); +this.submitEl=C.getElementById(this.id+B.SUBMIT_SUFFIX); +this.cancelEl=C.getElementById(this.id+B.CANCEL_SUFFIX); +if(YAHOO.env.ua.gecko&&this.yearEl&&this.yearEl.type=="text"){this.yearEl.setAttribute("autocomplete","off") +}this._setFirstLastElements() +},createMask:function(){var B=YAHOO.widget.CalendarNavigator.CLASSES; +var A=this._doc.createElement("div"); +A.className=B.MASK; +this.cal.oDomContainer.appendChild(A); +this.maskEl=A +},_syncMask:function(){var B=this.cal.oDomContainer; +if(B&&this.maskEl){var A=YAHOO.util.Dom.getRegion(B); +YAHOO.util.Dom.setStyle(this.maskEl,"width",A.right-A.left+"px"); +YAHOO.util.Dom.setStyle(this.maskEl,"height",A.bottom-A.top+"px") +}},renderNavContents:function(A){var D=YAHOO.widget.CalendarNavigator,E=D.CLASSES,B=A; +B[B.length]='<div class="'+E.MONTH+'">'; +this.renderMonth(B); +B[B.length]="</div>"; +B[B.length]='<div class="'+E.YEAR+'">'; +this.renderYear(B); +B[B.length]="</div>"; +B[B.length]='<div class="'+E.BUTTONS+'">'; +this.renderButtons(B); +B[B.length]="</div>"; +B[B.length]='<div class="'+E.ERROR+'" id="'+this.id+D.ERROR_SUFFIX+'"></div>'; +return B +},renderMonth:function(D){var G=YAHOO.widget.CalendarNavigator,H=G.CLASSES; +var I=this.id+G.MONTH_SUFFIX,F=this.__getCfg("monthFormat"),A=this.cal.cfg.getProperty((F==YAHOO.widget.Calendar.SHORT)?"MONTHS_SHORT":"MONTHS_LONG"),E=D; +if(A&&A.length>0){E[E.length]='<label for="'+I+'">'; +E[E.length]=this.__getCfg("month",true); +E[E.length]="</label>"; +E[E.length]='<select name="'+I+'" id="'+I+'" class="'+H.MONTH_CTRL+'">'; +for(var B=0; +B<A.length; +B++){E[E.length]='<option value="'+B+'">'; +E[E.length]=A[B]; +E[E.length]="</option>" +}E[E.length]="</select>" +}return E +},renderYear:function(B){var E=YAHOO.widget.CalendarNavigator,F=E.CLASSES; +var G=this.id+E.YEAR_SUFFIX,A=E.YR_MAX_DIGITS,D=B; +D[D.length]='<label for="'+G+'">'; +D[D.length]=this.__getCfg("year",true); +D[D.length]="</label>"; +D[D.length]='<input type="text" name="'+G+'" id="'+G+'" class="'+F.YEAR_CTRL+'" maxlength="'+A+'"/>'; +return D +},renderButtons:function(A){var D=YAHOO.widget.CalendarNavigator.CLASSES; +var B=A; +B[B.length]='<span class="'+D.BUTTON+" "+D.DEFAULT+'">'; +B[B.length]='<button type="button" id="'+this.id+'_submit">'; +B[B.length]=this.__getCfg("submit",true); +B[B.length]="</button>"; +B[B.length]="</span>"; +B[B.length]='<span class="'+D.BUTTON+'">'; +B[B.length]='<button type="button" id="'+this.id+'_cancel">'; +B[B.length]=this.__getCfg("cancel",true); +B[B.length]="</button>"; +B[B.length]="</span>"; +return B +},applyListeners:function(){var B=YAHOO.util.Event; +function A(){if(this.validate()){this.setYear(this._getYearFromUI()) +}}function C(){this.setMonth(this._getMonthFromUI()) +}B.on(this.submitEl,"click",this.submit,this,true); +B.on(this.cancelEl,"click",this.cancel,this,true); +B.on(this.yearEl,"blur",A,this,true); +B.on(this.monthEl,"change",C,this,true); +if(this.__isIEQuirks){YAHOO.util.Event.on(this.cal.oDomContainer,"resize",this._syncMask,this,true) +}this.applyKeyListeners() +},purgeListeners:function(){var A=YAHOO.util.Event; +A.removeListener(this.submitEl,"click",this.submit); +A.removeListener(this.cancelEl,"click",this.cancel); +A.removeListener(this.yearEl,"blur"); +A.removeListener(this.monthEl,"change"); +if(this.__isIEQuirks){A.removeListener(this.cal.oDomContainer,"resize",this._syncMask) +}this.purgeKeyListeners() +},applyKeyListeners:function(){var D=YAHOO.util.Event; +var A=YAHOO.env.ua; +var C=(A.ie)?"keydown":"keypress"; +var B=(A.ie||A.opera)?"keydown":"keypress"; +D.on(this.yearEl,"keypress",this._handleEnterKey,this,true); +D.on(this.yearEl,C,this._handleDirectionKeys,this,true); +D.on(this.lastCtrl,B,this._handleTabKey,this,true); +D.on(this.firstCtrl,B,this._handleShiftTabKey,this,true) +},purgeKeyListeners:function(){var C=YAHOO.util.Event; +var B=(YAHOO.env.ua.ie)?"keydown":"keypress"; +var A=(YAHOO.env.ua.ie||YAHOO.env.ua.opera)?"keydown":"keypress"; +C.removeListener(this.yearEl,"keypress",this._handleEnterKey); +C.removeListener(this.yearEl,B,this._handleDirectionKeys); +C.removeListener(this.lastCtrl,A,this._handleTabKey); +C.removeListener(this.firstCtrl,A,this._handleShiftTabKey) +},submit:function(){if(this.validate()){this.hide(); +this.setMonth(this._getMonthFromUI()); +this.setYear(this._getYearFromUI()); +var B=this.cal; +var C=this; +function D(){B.setYear(C.getYear()); +B.setMonth(C.getMonth()); +B.render() +}var A=YAHOO.widget.CalendarNavigator.UPDATE_DELAY; +if(A>0){window.setTimeout(D,A) +}else{D() +}}},cancel:function(){this.hide() +},validate:function(){if(this._getYearFromUI()!==null){this.clearErrors(); +return true +}else{this.setYearError(); +this.setError(this.__getCfg("invalidYear",true)); +return false +}},setError:function(A){if(this.errorEl){this.errorEl.innerHTML=A; +this._show(this.errorEl,true) +}},clearError:function(){if(this.errorEl){this.errorEl.innerHTML=""; +this._show(this.errorEl,false) +}},setYearError:function(){YAHOO.util.Dom.addClass(this.yearEl,YAHOO.widget.CalendarNavigator.CLASSES.INVALID) +},clearYearError:function(){YAHOO.util.Dom.removeClass(this.yearEl,YAHOO.widget.CalendarNavigator.CLASSES.INVALID) +},clearErrors:function(){this.clearError(); +this.clearYearError() +},setInitialFocus:function(){var A=this.submitEl; +var B=this.__getCfg("initialFocus"); +if(B&&B.toLowerCase){B=B.toLowerCase(); +if(B=="year"){A=this.yearEl; +try{this.yearEl.select() +}catch(C){}}else{if(B=="month"){A=this.monthEl +}}}if(A&&YAHOO.lang.isFunction(A.focus)){try{A.focus() +}catch(C){}}},erase:function(){if(this.__rendered){this.purgeListeners(); +this.yearEl=null; +this.monthEl=null; +this.errorEl=null; +this.submitEl=null; +this.cancelEl=null; +this.firstCtrl=null; +this.lastCtrl=null; +if(this.navEl){this.navEl.innerHTML="" +}var B=this.navEl.parentNode; +if(B){B.removeChild(this.navEl) +}this.navEl=null; +var A=this.maskEl.parentNode; +if(A){A.removeChild(this.maskEl) +}this.maskEl=null; +this.__rendered=false +}},destroy:function(){this.erase(); +this._doc=null; +this.cal=null; +this.id=null +},_show:function(B,A){if(B){YAHOO.util.Dom.setStyle(B,"display",(A)?"block":"none") +}},_getMonthFromUI:function(){if(this.monthEl){return this.monthEl.selectedIndex +}else{return 0 +}},_getYearFromUI:function(){var B=YAHOO.widget.CalendarNavigator; +var A=null; +if(this.yearEl){var C=this.yearEl.value; +C=C.replace(B.TRIM,"$1"); +if(B.YR_PATTERN.test(C)){A=parseInt(C,10) +}}return A +},_updateYearUI:function(){if(this.yearEl&&this._year!==null){this.yearEl.value=this._year +}},_updateMonthUI:function(){if(this.monthEl){this.monthEl.selectedIndex=this._month +}},_setFirstLastElements:function(){this.firstCtrl=this.monthEl; +this.lastCtrl=this.cancelEl; +if(this.__isMac){if(YAHOO.env.ua.webkit&&YAHOO.env.ua.webkit<420){this.firstCtrl=this.monthEl; +this.lastCtrl=this.yearEl +}if(YAHOO.env.ua.gecko){this.firstCtrl=this.yearEl; +this.lastCtrl=this.yearEl +}}},_handleEnterKey:function(B){var A=YAHOO.util.KeyListener.KEY; +if(YAHOO.util.Event.getCharCode(B)==A.ENTER){this.submit() +}},_handleDirectionKeys:function(G){var F=YAHOO.util.Event; +var A=YAHOO.util.KeyListener.KEY; +var C=YAHOO.widget.CalendarNavigator; +var D=(this.yearEl.value)?parseInt(this.yearEl.value,10):null; +if(isFinite(D)){var B=false; +switch(F.getCharCode(G)){case A.UP:this.yearEl.value=D+C.YR_MINOR_INC; +B=true; +break; +case A.DOWN:this.yearEl.value=Math.max(D-C.YR_MINOR_INC,0); +B=true; +break; +case A.PAGE_UP:this.yearEl.value=D+C.YR_MAJOR_INC; +B=true; +break; +case A.PAGE_DOWN:this.yearEl.value=Math.max(D-C.YR_MAJOR_INC,0); +B=true; +break; +default:break +}if(B){F.preventDefault(G); +try{this.yearEl.select() +}catch(G){}}}},_handleTabKey:function(C){var B=YAHOO.util.Event; +var A=YAHOO.util.KeyListener.KEY; +if(B.getCharCode(C)==A.TAB&&!C.shiftKey){try{B.preventDefault(C); +this.firstCtrl.focus() +}catch(C){}}},_handleShiftTabKey:function(C){var B=YAHOO.util.Event; +var A=YAHOO.util.KeyListener.KEY; +if(C.shiftKey&&B.getCharCode(C)==A.TAB){try{B.preventDefault(C); +this.lastCtrl.focus() +}catch(C){}}},__getCfg:function(D,B){var C=YAHOO.widget.CalendarNavigator._DEFAULT_CFG; +var A=this.cal.cfg.getProperty("navigator"); +if(B){return(A!==true&&A.strings&&A.strings[D])?A.strings[D]:C.strings[D] +}else{return(A!==true&&A[D])?A[D]:C[D] +}},__isMac:(navigator.userAgent.toLowerCase().indexOf("macintosh")!=-1)}; +YAHOO.register("calendar",YAHOO.widget.Calendar,{version:"2.4.1",build:"742"});
\ No newline at end of file diff --git a/sonar-server/src/main/webapp/javascripts/calendar/calendar.js b/sonar-server/src/main/webapp/javascripts/calendar/calendar.js deleted file mode 100644 index e76a332cbe2..00000000000 --- a/sonar-server/src/main/webapp/javascripts/calendar/calendar.js +++ /dev/null @@ -1,6759 +0,0 @@ -/* -Copyright (c) 2007, Yahoo! Inc. All rights reserved. -Code licensed under the BSD License: -http://developer.yahoo.net/yui/license.txt -version: 2.4.1 -*/ -(function () { - - /** - * Config is a utility used within an Object to allow the implementer to - * maintain a list of local configuration properties and listen for changes - * to those properties dynamically using CustomEvent. The initial values are - * also maintained so that the configuration can be reset at any given point - * to its initial state. - * @namespace YAHOO.util - * @class Config - * @constructor - * @param {Object} owner The owner Object to which this Config Object belongs - */ - YAHOO.util.Config = function (owner) { - - if (owner) { - this.init(owner); - } - - - }; - - - var Lang = YAHOO.lang, - CustomEvent = YAHOO.util.CustomEvent, - Config = YAHOO.util.Config; - - - /** - * Constant representing the CustomEvent type for the config changed event. - * @property YAHOO.util.Config.CONFIG_CHANGED_EVENT - * @private - * @static - * @final - */ - Config.CONFIG_CHANGED_EVENT = "configChanged"; - - /** - * Constant representing the boolean type string - * @property YAHOO.util.Config.BOOLEAN_TYPE - * @private - * @static - * @final - */ - Config.BOOLEAN_TYPE = "boolean"; - - Config.prototype = { - - /** - * Object reference to the owner of this Config Object - * @property owner - * @type Object - */ - owner: null, - - /** - * Boolean flag that specifies whether a queue is currently - * being executed - * @property queueInProgress - * @type Boolean - */ - queueInProgress: false, - - /** - * Maintains the local collection of configuration property objects and - * their specified values - * @property config - * @private - * @type Object - */ - config: null, - - /** - * Maintains the local collection of configuration property objects as - * they were initially applied. - * This object is used when resetting a property. - * @property initialConfig - * @private - * @type Object - */ - initialConfig: null, - - /** - * Maintains the local, normalized CustomEvent queue - * @property eventQueue - * @private - * @type Object - */ - eventQueue: null, - - /** - * Custom Event, notifying subscribers when Config properties are set - * (setProperty is called without the silent flag - * @event configChangedEvent - */ - configChangedEvent: null, - - /** - * Initializes the configuration Object and all of its local members. - * @method init - * @param {Object} owner The owner Object to which this Config - * Object belongs - */ - init: function (owner) { - - this.owner = owner; - - this.configChangedEvent = - this.createEvent(Config.CONFIG_CHANGED_EVENT); - - this.configChangedEvent.signature = CustomEvent.LIST; - this.queueInProgress = false; - this.config = {}; - this.initialConfig = {}; - this.eventQueue = []; - - }, - - /** - * Validates that the value passed in is a Boolean. - * @method checkBoolean - * @param {Object} val The value to validate - * @return {Boolean} true, if the value is valid - */ - checkBoolean: function (val) { - return (typeof val == Config.BOOLEAN_TYPE); - }, - - /** - * Validates that the value passed in is a number. - * @method checkNumber - * @param {Object} val The value to validate - * @return {Boolean} true, if the value is valid - */ - checkNumber: function (val) { - return (!isNaN(val)); - }, - - /** - * Fires a configuration property event using the specified value. - * @method fireEvent - * @private - * @param {String} key The configuration property's name - * @param {value} Object The value of the correct type for the property - */ - fireEvent: function ( key, value ) { - var property = this.config[key]; - - if (property && property.event) { - property.event.fire(value); - } - }, - - /** - * Adds a property to the Config Object's private config hash. - * @method addProperty - * @param {String} key The configuration property's name - * @param {Object} propertyObject The Object containing all of this - * property's arguments - */ - addProperty: function ( key, propertyObject ) { - key = key.toLowerCase(); - - this.config[key] = propertyObject; - - propertyObject.event = this.createEvent(key, { scope: this.owner }); - propertyObject.event.signature = CustomEvent.LIST; - - - propertyObject.key = key; - - if (propertyObject.handler) { - propertyObject.event.subscribe(propertyObject.handler, - this.owner); - } - - this.setProperty(key, propertyObject.value, true); - - if (! propertyObject.suppressEvent) { - this.queueProperty(key, propertyObject.value); - } - - }, - - /** - * Returns a key-value configuration map of the values currently set in - * the Config Object. - * @method getConfig - * @return {Object} The current config, represented in a key-value map - */ - getConfig: function () { - - var cfg = {}, - prop, - property; - - for (prop in this.config) { - property = this.config[prop]; - if (property && property.event) { - cfg[prop] = property.value; - } - } - - return cfg; - }, - - /** - * Returns the value of specified property. - * @method getProperty - * @param {String} key The name of the property - * @return {Object} The value of the specified property - */ - getProperty: function (key) { - var property = this.config[key.toLowerCase()]; - if (property && property.event) { - return property.value; - } else { - return undefined; - } - }, - - /** - * Resets the specified property's value to its initial value. - * @method resetProperty - * @param {String} key The name of the property - * @return {Boolean} True is the property was reset, false if not - */ - resetProperty: function (key) { - - key = key.toLowerCase(); - - var property = this.config[key]; - - if (property && property.event) { - - if (this.initialConfig[key] && - !Lang.isUndefined(this.initialConfig[key])) { - - this.setProperty(key, this.initialConfig[key]); - - return true; - - } - - } else { - - return false; - } - - }, - - /** - * Sets the value of a property. If the silent property is passed as - * true, the property's event will not be fired. - * @method setProperty - * @param {String} key The name of the property - * @param {String} value The value to set the property to - * @param {Boolean} silent Whether the value should be set silently, - * without firing the property event. - * @return {Boolean} True, if the set was successful, false if it failed. - */ - setProperty: function (key, value, silent) { - - var property; - - key = key.toLowerCase(); - - if (this.queueInProgress && ! silent) { - // Currently running through a queue... - this.queueProperty(key,value); - return true; - - } else { - property = this.config[key]; - if (property && property.event) { - if (property.validator && !property.validator(value)) { - return false; - } else { - property.value = value; - if (! silent) { - this.fireEvent(key, value); - this.configChangedEvent.fire([key, value]); - } - return true; - } - } else { - return false; - } - } - }, - - /** - * Sets the value of a property and queues its event to execute. If the - * event is already scheduled to execute, it is - * moved from its current position to the end of the queue. - * @method queueProperty - * @param {String} key The name of the property - * @param {String} value The value to set the property to - * @return {Boolean} true, if the set was successful, false if - * it failed. - */ - queueProperty: function (key, value) { - - key = key.toLowerCase(); - - var property = this.config[key], - foundDuplicate = false, - iLen, - queueItem, - queueItemKey, - queueItemValue, - sLen, - supercedesCheck, - qLen, - queueItemCheck, - queueItemCheckKey, - queueItemCheckValue, - i, - s, - q; - - if (property && property.event) { - - if (!Lang.isUndefined(value) && property.validator && - !property.validator(value)) { // validator - return false; - } else { - - if (!Lang.isUndefined(value)) { - property.value = value; - } else { - value = property.value; - } - - foundDuplicate = false; - iLen = this.eventQueue.length; - - for (i = 0; i < iLen; i++) { - queueItem = this.eventQueue[i]; - - if (queueItem) { - queueItemKey = queueItem[0]; - queueItemValue = queueItem[1]; - - if (queueItemKey == key) { - - /* - found a dupe... push to end of queue, null - current item, and break - */ - - this.eventQueue[i] = null; - - this.eventQueue.push( - [key, (!Lang.isUndefined(value) ? - value : queueItemValue)]); - - foundDuplicate = true; - break; - } - } - } - - // this is a refire, or a new property in the queue - - if (! foundDuplicate && !Lang.isUndefined(value)) { - this.eventQueue.push([key, value]); - } - } - - if (property.supercedes) { - - sLen = property.supercedes.length; - - for (s = 0; s < sLen; s++) { - - supercedesCheck = property.supercedes[s]; - qLen = this.eventQueue.length; - - for (q = 0; q < qLen; q++) { - queueItemCheck = this.eventQueue[q]; - - if (queueItemCheck) { - queueItemCheckKey = queueItemCheck[0]; - queueItemCheckValue = queueItemCheck[1]; - - if (queueItemCheckKey == - supercedesCheck.toLowerCase() ) { - - this.eventQueue.push([queueItemCheckKey, - queueItemCheckValue]); - - this.eventQueue[q] = null; - break; - - } - } - } - } - } - - - return true; - } else { - return false; - } - }, - - /** - * Fires the event for a property using the property's current value. - * @method refireEvent - * @param {String} key The name of the property - */ - refireEvent: function (key) { - - key = key.toLowerCase(); - - var property = this.config[key]; - - if (property && property.event && - - !Lang.isUndefined(property.value)) { - - if (this.queueInProgress) { - - this.queueProperty(key); - - } else { - - this.fireEvent(key, property.value); - - } - - } - }, - - /** - * Applies a key-value Object literal to the configuration, replacing - * any existing values, and queueing the property events. - * Although the values will be set, fireQueue() must be called for their - * associated events to execute. - * @method applyConfig - * @param {Object} userConfig The configuration Object literal - * @param {Boolean} init When set to true, the initialConfig will - * be set to the userConfig passed in, so that calling a reset will - * reset the properties to the passed values. - */ - applyConfig: function (userConfig, init) { - - var sKey, - oConfig; - - if (init) { - oConfig = {}; - for (sKey in userConfig) { - if (Lang.hasOwnProperty(userConfig, sKey)) { - oConfig[sKey.toLowerCase()] = userConfig[sKey]; - } - } - this.initialConfig = oConfig; - } - - for (sKey in userConfig) { - if (Lang.hasOwnProperty(userConfig, sKey)) { - this.queueProperty(sKey, userConfig[sKey]); - } - } - }, - - /** - * Refires the events for all configuration properties using their - * current values. - * @method refresh - */ - refresh: function () { - - var prop; - - for (prop in this.config) { - this.refireEvent(prop); - } - }, - - /** - * Fires the normalized list of queued property change events - * @method fireQueue - */ - fireQueue: function () { - - var i, - queueItem, - key, - value, - property; - - this.queueInProgress = true; - for (i = 0;i < this.eventQueue.length; i++) { - queueItem = this.eventQueue[i]; - if (queueItem) { - - key = queueItem[0]; - value = queueItem[1]; - property = this.config[key]; - - property.value = value; - - this.fireEvent(key,value); - } - } - - this.queueInProgress = false; - this.eventQueue = []; - }, - - /** - * Subscribes an external handler to the change event for any - * given property. - * @method subscribeToConfigEvent - * @param {String} key The property name - * @param {Function} handler The handler function to use subscribe to - * the property's event - * @param {Object} obj The Object to use for scoping the event handler - * (see CustomEvent documentation) - * @param {Boolean} override Optional. If true, will override "this" - * within the handler to map to the scope Object passed into the method. - * @return {Boolean} True, if the subscription was successful, - * otherwise false. - */ - subscribeToConfigEvent: function (key, handler, obj, override) { - - var property = this.config[key.toLowerCase()]; - - if (property && property.event) { - if (!Config.alreadySubscribed(property.event, handler, obj)) { - property.event.subscribe(handler, obj, override); - } - return true; - } else { - return false; - } - - }, - - /** - * Unsubscribes an external handler from the change event for any - * given property. - * @method unsubscribeFromConfigEvent - * @param {String} key The property name - * @param {Function} handler The handler function to use subscribe to - * the property's event - * @param {Object} obj The Object to use for scoping the event - * handler (see CustomEvent documentation) - * @return {Boolean} True, if the unsubscription was successful, - * otherwise false. - */ - unsubscribeFromConfigEvent: function (key, handler, obj) { - var property = this.config[key.toLowerCase()]; - if (property && property.event) { - return property.event.unsubscribe(handler, obj); - } else { - return false; - } - }, - - /** - * Returns a string representation of the Config object - * @method toString - * @return {String} The Config object in string format. - */ - toString: function () { - var output = "Config"; - if (this.owner) { - output += " [" + this.owner.toString() + "]"; - } - return output; - }, - - /** - * Returns a string representation of the Config object's current - * CustomEvent queue - * @method outputEventQueue - * @return {String} The string list of CustomEvents currently queued - * for execution - */ - outputEventQueue: function () { - - var output = "", - queueItem, - q, - nQueue = this.eventQueue.length; - - for (q = 0; q < nQueue; q++) { - queueItem = this.eventQueue[q]; - if (queueItem) { - output += queueItem[0] + "=" + queueItem[1] + ", "; - } - } - return output; - }, - - /** - * Sets all properties to null, unsubscribes all listeners from each - * property's change event and all listeners from the configChangedEvent. - * @method destroy - */ - destroy: function () { - - var oConfig = this.config, - sProperty, - oProperty; - - - for (sProperty in oConfig) { - - if (Lang.hasOwnProperty(oConfig, sProperty)) { - - oProperty = oConfig[sProperty]; - - oProperty.event.unsubscribeAll(); - oProperty.event = null; - - } - - } - - this.configChangedEvent.unsubscribeAll(); - - this.configChangedEvent = null; - this.owner = null; - this.config = null; - this.initialConfig = null; - this.eventQueue = null; - - } - - }; - - - - /** - * Checks to determine if a particular function/Object pair are already - * subscribed to the specified CustomEvent - * @method YAHOO.util.Config.alreadySubscribed - * @static - * @param {YAHOO.util.CustomEvent} evt The CustomEvent for which to check - * the subscriptions - * @param {Function} fn The function to look for in the subscribers list - * @param {Object} obj The execution scope Object for the subscription - * @return {Boolean} true, if the function/Object pair is already subscribed - * to the CustomEvent passed in - */ - Config.alreadySubscribed = function (evt, fn, obj) { - - var nSubscribers = evt.subscribers.length, - subsc, - i; - - if (nSubscribers > 0) { - i = nSubscribers - 1; - do { - subsc = evt.subscribers[i]; - if (subsc && subsc.obj == obj && subsc.fn == fn) { - return true; - } - } - while (i--); - } - - return false; - - }; - - YAHOO.lang.augmentProto(Config, YAHOO.util.EventProvider); - -}()); - -/** -* YAHOO.widget.DateMath is used for simple date manipulation. The class is a static utility -* used for adding, subtracting, and comparing dates. -* @namespace YAHOO.widget -* @class DateMath -*/ -YAHOO.widget.DateMath = { - /** - * Constant field representing Day - * @property DAY - * @static - * @final - * @type String - */ - DAY : "D", - - /** - * Constant field representing Week - * @property WEEK - * @static - * @final - * @type String - */ - WEEK : "W", - - /** - * Constant field representing Year - * @property YEAR - * @static - * @final - * @type String - */ - YEAR : "Y", - - /** - * Constant field representing Month - * @property MONTH - * @static - * @final - * @type String - */ - MONTH : "M", - - /** - * Constant field representing one day, in milliseconds - * @property ONE_DAY_MS - * @static - * @final - * @type Number - */ - ONE_DAY_MS : 1000*60*60*24, - - /** - * Adds the specified amount of time to the this instance. - * @method add - * @param {Date} date The JavaScript Date object to perform addition on - * @param {String} field The field constant to be used for performing addition. - * @param {Number} amount The number of units (measured in the field constant) to add to the date. - * @return {Date} The resulting Date object - */ - add : function(date, field, amount) { - var d = new Date(date.getTime()); - switch (field) { - case this.MONTH: - var newMonth = date.getMonth() + amount; - var years = 0; - - - if (newMonth < 0) { - while (newMonth < 0) { - newMonth += 12; - years -= 1; - } - } else if (newMonth > 11) { - while (newMonth > 11) { - newMonth -= 12; - years += 1; - } - } - - d.setMonth(newMonth); - d.setFullYear(date.getFullYear() + years); - break; - case this.DAY: - d.setDate(date.getDate() + amount); - break; - case this.YEAR: - d.setFullYear(date.getFullYear() + amount); - break; - case this.WEEK: - d.setDate(date.getDate() + (amount * 7)); - break; - } - return d; - }, - - /** - * Subtracts the specified amount of time from the this instance. - * @method subtract - * @param {Date} date The JavaScript Date object to perform subtraction on - * @param {Number} field The this field constant to be used for performing subtraction. - * @param {Number} amount The number of units (measured in the field constant) to subtract from the date. - * @return {Date} The resulting Date object - */ - subtract : function(date, field, amount) { - return this.add(date, field, (amount*-1)); - }, - - /** - * Determines whether a given date is before another date on the calendar. - * @method before - * @param {Date} date The Date object to compare with the compare argument - * @param {Date} compareTo The Date object to use for the comparison - * @return {Boolean} true if the date occurs before the compared date; false if not. - */ - before : function(date, compareTo) { - var ms = compareTo.getTime(); - if (date.getTime() < ms) { - return true; - } else { - return false; - } - }, - - /** - * Determines whether a given date is after another date on the calendar. - * @method after - * @param {Date} date The Date object to compare with the compare argument - * @param {Date} compareTo The Date object to use for the comparison - * @return {Boolean} true if the date occurs after the compared date; false if not. - */ - after : function(date, compareTo) { - var ms = compareTo.getTime(); - if (date.getTime() > ms) { - return true; - } else { - return false; - } - }, - - /** - * Determines whether a given date is between two other dates on the calendar. - * @method between - * @param {Date} date The date to check for - * @param {Date} dateBegin The start of the range - * @param {Date} dateEnd The end of the range - * @return {Boolean} true if the date occurs between the compared dates; false if not. - */ - between : function(date, dateBegin, dateEnd) { - if (this.after(date, dateBegin) && this.before(date, dateEnd)) { - return true; - } else { - return false; - } - }, - - /** - * Retrieves a JavaScript Date object representing January 1 of any given year. - * @method getJan1 - * @param {Number} calendarYear The calendar year for which to retrieve January 1 - * @return {Date} January 1 of the calendar year specified. - */ - getJan1 : function(calendarYear) { - return this.getDate(calendarYear,0,1); - }, - - /** - * Calculates the number of days the specified date is from January 1 of the specified calendar year. - * Passing January 1 to this function would return an offset value of zero. - * @method getDayOffset - * @param {Date} date The JavaScript date for which to find the offset - * @param {Number} calendarYear The calendar year to use for determining the offset - * @return {Number} The number of days since January 1 of the given year - */ - getDayOffset : function(date, calendarYear) { - var beginYear = this.getJan1(calendarYear); // Find the start of the year. This will be in week 1. - - // Find the number of days the passed in date is away from the calendar year start - var dayOffset = Math.ceil((date.getTime()-beginYear.getTime()) / this.ONE_DAY_MS); - return dayOffset; - }, - - /** - * Calculates the week number for the given date. This function assumes that week 1 is the - * week in which January 1 appears, regardless of whether the week consists of a full 7 days. - * The calendar year can be specified to help find what a the week number would be for a given - * date if the date overlaps years. For instance, a week may be considered week 1 of 2005, or - * week 53 of 2004. Specifying the optional calendarYear allows one to make this distinction - * easily. - * @method getWeekNumber - * @param {Date} date The JavaScript date for which to find the week number - * @param {Number} calendarYear OPTIONAL - The calendar year to use for determining the week number. Default is - * the calendar year of parameter "date". - * @return {Number} The week number of the given date. - */ - getWeekNumber : function(date, calendarYear) { - date = this.clearTime(date); - var nearestThurs = new Date(date.getTime() + (4 * this.ONE_DAY_MS) - ((date.getDay()) * this.ONE_DAY_MS)); - - var jan1 = this.getDate(nearestThurs.getFullYear(),0,1); - var dayOfYear = ((nearestThurs.getTime() - jan1.getTime()) / this.ONE_DAY_MS) - 1; - - var weekNum = Math.ceil((dayOfYear)/ 7); - return weekNum; - }, - - /** - * Determines if a given week overlaps two different years. - * @method isYearOverlapWeek - * @param {Date} weekBeginDate The JavaScript Date representing the first day of the week. - * @return {Boolean} true if the date overlaps two different years. - */ - isYearOverlapWeek : function(weekBeginDate) { - var overlaps = false; - var nextWeek = this.add(weekBeginDate, this.DAY, 6); - if (nextWeek.getFullYear() != weekBeginDate.getFullYear()) { - overlaps = true; - } - return overlaps; - }, - - /** - * Determines if a given week overlaps two different months. - * @method isMonthOverlapWeek - * @param {Date} weekBeginDate The JavaScript Date representing the first day of the week. - * @return {Boolean} true if the date overlaps two different months. - */ - isMonthOverlapWeek : function(weekBeginDate) { - var overlaps = false; - var nextWeek = this.add(weekBeginDate, this.DAY, 6); - if (nextWeek.getMonth() != weekBeginDate.getMonth()) { - overlaps = true; - } - return overlaps; - }, - - /** - * Gets the first day of a month containing a given date. - * @method findMonthStart - * @param {Date} date The JavaScript Date used to calculate the month start - * @return {Date} The JavaScript Date representing the first day of the month - */ - findMonthStart : function(date) { - var start = this.getDate(date.getFullYear(), date.getMonth(), 1); - return start; - }, - - /** - * Gets the last day of a month containing a given date. - * @method findMonthEnd - * @param {Date} date The JavaScript Date used to calculate the month end - * @return {Date} The JavaScript Date representing the last day of the month - */ - findMonthEnd : function(date) { - var start = this.findMonthStart(date); - var nextMonth = this.add(start, this.MONTH, 1); - var end = this.subtract(nextMonth, this.DAY, 1); - return end; - }, - - /** - * Clears the time fields from a given date, effectively setting the time to 12 noon. - * @method clearTime - * @param {Date} date The JavaScript Date for which the time fields will be cleared - * @return {Date} The JavaScript Date cleared of all time fields - */ - clearTime : function(date) { - date.setHours(12,0,0,0); - return date; - }, - - /** - * Returns a new JavaScript Date object, representing the given year, month and date. Time fields (hr, min, sec, ms) on the new Date object - * are set to 0. The method allows Date instances to be created with the a year less than 100. "new Date(year, month, date)" implementations - * set the year to 19xx if a year (xx) which is less than 100 is provided. - * <p> - * <em>NOTE:</em>Validation on argument values is not performed. It is the caller's responsibility to ensure - * arguments are valid as per the ECMAScript-262 Date object specification for the new Date(year, month[, date]) constructor. - * </p> - * @method getDate - * @param {Number} y Year. - * @param {Number} m Month index from 0 (Jan) to 11 (Dec). - * @param {Number} d (optional) Date from 1 to 31. If not provided, defaults to 1. - * @return {Date} The JavaScript date object with year, month, date set as provided. - */ - getDate : function(y, m, d) { - var dt = null; - if (YAHOO.lang.isUndefined(d)) { - d = 1; - } - if (y >= 100) { - dt = new Date(y, m, d); - } else { - dt = new Date(); - dt.setFullYear(y); - dt.setMonth(m); - dt.setDate(d); - dt.setHours(0,0,0,0); - } - return dt; - } -}; - -/** -* The Calendar component is a UI control that enables users to choose one or more dates from a graphical calendar presented in a one-month or -* multi-month interface. Calendars are generated entirely via script and can be navigated without any page refreshes. -* @module calendar -* @title Calendar -* @namespace YAHOO.widget -* @requires yahoo,dom,event -*/ - -/** -* Calendar is the base class for the Calendar widget. In its most basic -* implementation, it has the ability to render a calendar widget on the page -* that can be manipulated to select a single date, move back and forth between -* months and years. -* <p>To construct the placeholder for the calendar widget, the code is as -* follows: -* <xmp> -* <div id="calContainer"></div> -* </xmp> -* </p> -* <p> -* <strong>NOTE: As of 2.4.0, the constructor's ID argument is optional.</strong> -* The Calendar can be constructed by simply providing a container ID string, -* or a reference to a container DIV HTMLElement (the element needs to exist -* in the document). -* -* E.g.: -* <xmp> -* var c = new YAHOO.widget.Calendar("calContainer", configOptions); -* </xmp> -* or: -* <xmp> -* var containerDiv = YAHOO.util.Dom.get("calContainer"); -* var c = new YAHOO.widget.Calendar(containerDiv, configOptions); -* </xmp> -* </p> -* <p> -* If not provided, the ID will be generated from the container DIV ID by adding an "_t" suffix. -* For example if an ID is not provided, and the container's ID is "calContainer", the Calendar's ID will be set to "calContainer_t". -* </p> -* -* @namespace YAHOO.widget -* @class Calendar -* @constructor -* @param {String} id optional The id of the table element that will represent the Calendar widget. As of 2.4.0, this argument is optional. -* @param {String | HTMLElement} container The id of the container div element that will wrap the Calendar table, or a reference to a DIV element which exists in the document. -* @param {Object} config optional The configuration object containing the initial configuration values for the Calendar. -*/ -YAHOO.widget.Calendar = function(id, containerId, config) { - this.init.apply(this, arguments); -}; - -/** -* The path to be used for images loaded for the Calendar -* @property YAHOO.widget.Calendar.IMG_ROOT -* @static -* @deprecated You can now customize images by overriding the calclose, calnavleft and calnavright default CSS classes for the close icon, left arrow and right arrow respectively -* @type String -*/ -YAHOO.widget.Calendar.IMG_ROOT = null; - -/** -* Type constant used for renderers to represent an individual date (M/D/Y) -* @property YAHOO.widget.Calendar.DATE -* @static -* @final -* @type String -*/ -YAHOO.widget.Calendar.DATE = "D"; - -/** -* Type constant used for renderers to represent an individual date across any year (M/D) -* @property YAHOO.widget.Calendar.MONTH_DAY -* @static -* @final -* @type String -*/ -YAHOO.widget.Calendar.MONTH_DAY = "MD"; - -/** -* Type constant used for renderers to represent a weekday -* @property YAHOO.widget.Calendar.WEEKDAY -* @static -* @final -* @type String -*/ -YAHOO.widget.Calendar.WEEKDAY = "WD"; - -/** -* Type constant used for renderers to represent a range of individual dates (M/D/Y-M/D/Y) -* @property YAHOO.widget.Calendar.RANGE -* @static -* @final -* @type String -*/ -YAHOO.widget.Calendar.RANGE = "R"; - -/** -* Type constant used for renderers to represent a month across any year -* @property YAHOO.widget.Calendar.MONTH -* @static -* @final -* @type String -*/ -YAHOO.widget.Calendar.MONTH = "M"; - -/** -* Constant that represents the total number of date cells that are displayed in a given month -* @property YAHOO.widget.Calendar.DISPLAY_DAYS -* @static -* @final -* @type Number -*/ -YAHOO.widget.Calendar.DISPLAY_DAYS = 42; - -/** -* Constant used for halting the execution of the remainder of the render stack -* @property YAHOO.widget.Calendar.STOP_RENDER -* @static -* @final -* @type String -*/ -YAHOO.widget.Calendar.STOP_RENDER = "S"; - -/** -* Constant used to represent short date field string formats (e.g. Tu or Feb) -* @property YAHOO.widget.Calendar.SHORT -* @static -* @final -* @type String -*/ -YAHOO.widget.Calendar.SHORT = "short"; - -/** -* Constant used to represent long date field string formats (e.g. Monday or February) -* @property YAHOO.widget.Calendar.LONG -* @static -* @final -* @type String -*/ -YAHOO.widget.Calendar.LONG = "long"; - -/** -* Constant used to represent medium date field string formats (e.g. Mon) -* @property YAHOO.widget.Calendar.MEDIUM -* @static -* @final -* @type String -*/ -YAHOO.widget.Calendar.MEDIUM = "medium"; - -/** -* Constant used to represent single character date field string formats (e.g. M, T, W) -* @property YAHOO.widget.Calendar.ONE_CHAR -* @static -* @final -* @type String -*/ -YAHOO.widget.Calendar.ONE_CHAR = "1char"; - -/** -* The set of default Config property keys and values for the Calendar -* @property YAHOO.widget.Calendar._DEFAULT_CONFIG -* @final -* @static -* @private -* @type Object -*/ -YAHOO.widget.Calendar._DEFAULT_CONFIG = { - // Default values for pagedate and selected are not class level constants - they are set during instance creation - PAGEDATE : {key:"pagedate", value:null}, - SELECTED : {key:"selected", value:null}, - TITLE : {key:"title", value:""}, - CLOSE : {key:"close", value:false}, - IFRAME : {key:"iframe", value:(YAHOO.env.ua.ie && YAHOO.env.ua.ie <= 6) ? true : false}, - MINDATE : {key:"mindate", value:null}, - MAXDATE : {key:"maxdate", value:null}, - MULTI_SELECT : {key:"multi_select", value:false}, - START_WEEKDAY : {key:"start_weekday", value:0}, - SHOW_WEEKDAYS : {key:"show_weekdays", value:true}, - SHOW_WEEK_HEADER : {key:"show_week_header", value:false}, - SHOW_WEEK_FOOTER : {key:"show_week_footer", value:false}, - HIDE_BLANK_WEEKS : {key:"hide_blank_weeks", value:false}, - NAV_ARROW_LEFT: {key:"nav_arrow_left", value:null} , - NAV_ARROW_RIGHT : {key:"nav_arrow_right", value:null} , - MONTHS_SHORT : {key:"months_short", value:["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]}, - MONTHS_LONG: {key:"months_long", value:["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]}, - WEEKDAYS_1CHAR: {key:"weekdays_1char", value:["S", "M", "T", "W", "T", "F", "S"]}, - WEEKDAYS_SHORT: {key:"weekdays_short", value:["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"]}, - WEEKDAYS_MEDIUM: {key:"weekdays_medium", value:["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]}, - WEEKDAYS_LONG: {key:"weekdays_long", value:["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]}, - LOCALE_MONTHS:{key:"locale_months", value:"long"}, - LOCALE_WEEKDAYS:{key:"locale_weekdays", value:"short"}, - DATE_DELIMITER:{key:"date_delimiter", value:","}, - DATE_FIELD_DELIMITER:{key:"date_field_delimiter", value:"/"}, - DATE_RANGE_DELIMITER:{key:"date_range_delimiter", value:"-"}, - MY_MONTH_POSITION:{key:"my_month_position", value:1}, - MY_YEAR_POSITION:{key:"my_year_position", value:2}, - MD_MONTH_POSITION:{key:"md_month_position", value:1}, - MD_DAY_POSITION:{key:"md_day_position", value:2}, - MDY_MONTH_POSITION:{key:"mdy_month_position", value:1}, - MDY_DAY_POSITION:{key:"mdy_day_position", value:2}, - MDY_YEAR_POSITION:{key:"mdy_year_position", value:3}, - MY_LABEL_MONTH_POSITION:{key:"my_label_month_position", value:1}, - MY_LABEL_YEAR_POSITION:{key:"my_label_year_position", value:2}, - MY_LABEL_MONTH_SUFFIX:{key:"my_label_month_suffix", value:" "}, - MY_LABEL_YEAR_SUFFIX:{key:"my_label_year_suffix", value:""}, - NAV: {key:"navigator", value: null} -}; - -/** -* The set of Custom Event types supported by the Calendar -* @property YAHOO.widget.Calendar._EVENT_TYPES -* @final -* @static -* @private -* @type Object -*/ -YAHOO.widget.Calendar._EVENT_TYPES = { - BEFORE_SELECT : "beforeSelect", - SELECT : "select", - BEFORE_DESELECT : "beforeDeselect", - DESELECT : "deselect", - CHANGE_PAGE : "changePage", - BEFORE_RENDER : "beforeRender", - RENDER : "render", - RESET : "reset", - CLEAR : "clear", - BEFORE_HIDE : "beforeHide", - HIDE : "hide", - BEFORE_SHOW : "beforeShow", - SHOW : "show", - BEFORE_HIDE_NAV : "beforeHideNav", - HIDE_NAV : "hideNav", - BEFORE_SHOW_NAV : "beforeShowNav", - SHOW_NAV : "showNav", - BEFORE_RENDER_NAV : "beforeRenderNav", - RENDER_NAV : "renderNav" -}; - -/** -* The set of default style constants for the Calendar -* @property YAHOO.widget.Calendar._STYLES -* @final -* @static -* @private -* @type Object -*/ -YAHOO.widget.Calendar._STYLES = { - CSS_ROW_HEADER: "calrowhead", - CSS_ROW_FOOTER: "calrowfoot", - CSS_CELL : "calcell", - CSS_CELL_SELECTOR : "selector", - CSS_CELL_SELECTED : "selected", - CSS_CELL_SELECTABLE : "selectable", - CSS_CELL_RESTRICTED : "restricted", - CSS_CELL_TODAY : "today", - CSS_CELL_OOM : "oom", - CSS_CELL_OOB : "previous", - CSS_HEADER : "calheader", - CSS_HEADER_TEXT : "calhead", - CSS_BODY : "calbody", - CSS_WEEKDAY_CELL : "calweekdaycell", - CSS_WEEKDAY_ROW : "calweekdayrow", - CSS_FOOTER : "calfoot", - CSS_CALENDAR : "yui-calendar", - CSS_SINGLE : "single", - CSS_CONTAINER : "yui-calcontainer", - CSS_NAV_LEFT : "calnavleft", - CSS_NAV_RIGHT : "calnavright", - CSS_NAV : "calnav", - CSS_CLOSE : "calclose", - CSS_CELL_TOP : "calcelltop", - CSS_CELL_LEFT : "calcellleft", - CSS_CELL_RIGHT : "calcellright", - CSS_CELL_BOTTOM : "calcellbottom", - CSS_CELL_HOVER : "calcellhover", - CSS_CELL_HIGHLIGHT1 : "highlight1", - CSS_CELL_HIGHLIGHT2 : "highlight2", - CSS_CELL_HIGHLIGHT3 : "highlight3", - CSS_CELL_HIGHLIGHT4 : "highlight4" -}; - -YAHOO.widget.Calendar.prototype = { - - /** - * The configuration object used to set up the calendars various locale and style options. - * @property Config - * @private - * @deprecated Configuration properties should be set by calling Calendar.cfg.setProperty. - * @type Object - */ - Config : null, - - /** - * The parent CalendarGroup, only to be set explicitly by the parent group - * @property parent - * @type CalendarGroup - */ - parent : null, - - /** - * The index of this item in the parent group - * @property index - * @type Number - */ - index : -1, - - /** - * The collection of calendar table cells - * @property cells - * @type HTMLTableCellElement[] - */ - cells : null, - - /** - * The collection of calendar cell dates that is parallel to the cells collection. The array contains dates field arrays in the format of [YYYY, M, D]. - * @property cellDates - * @type Array[](Number[]) - */ - cellDates : null, - - /** - * The id that uniquely identifies this Calendar. - * @property id - * @type String - */ - id : null, - - /** - * The unique id associated with the Calendar's container - * @property containerId - * @type String - */ - containerId: null, - - /** - * The DOM element reference that points to this calendar's container element. The calendar will be inserted into this element when the shell is rendered. - * @property oDomContainer - * @type HTMLElement - */ - oDomContainer : null, - - /** - * A Date object representing today's date. - * @property today - * @type Date - */ - today : null, - - /** - * The list of render functions, along with required parameters, used to render cells. - * @property renderStack - * @type Array[] - */ - renderStack : null, - - /** - * A copy of the initial render functions created before rendering. - * @property _renderStack - * @private - * @type Array - */ - _renderStack : null, - - /** - * A reference to the CalendarNavigator instance created for this Calendar. - * Will be null if the "navigator" configuration property has not been set - * @property oNavigator - * @type CalendarNavigator - */ - oNavigator : null, - - /** - * The private list of initially selected dates. - * @property _selectedDates - * @private - * @type Array - */ - _selectedDates : null, - - /** - * A map of DOM event handlers to attach to cells associated with specific CSS class names - * @property domEventMap - * @type Object - */ - domEventMap : null, - - /** - * Protected helper used to parse Calendar constructor/init arguments. - * - * As of 2.4.0, Calendar supports a simpler constructor - * signature. This method reconciles arguments - * received in the pre 2.4.0 and 2.4.0 formats. - * - * @protected - * @method _parseArgs - * @param {Array} Function "arguments" array - * @return {Object} Object with id, container, config properties containing - * the reconciled argument values. - **/ - _parseArgs : function(args) { - /* - 2.4.0 Constructors signatures - - new Calendar(String) - new Calendar(HTMLElement) - new Calendar(String, ConfigObject) - new Calendar(HTMLElement, ConfigObject) - - Pre 2.4.0 Constructor signatures - - new Calendar(String, String) - new Calendar(String, HTMLElement) - new Calendar(String, String, ConfigObject) - new Calendar(String, HTMLElement, ConfigObject) - */ - var nArgs = {id:null, container:null, config:null}; - - if (args && args.length && args.length > 0) { - switch (args.length) { - case 1: - nArgs.id = null; - nArgs.container = args[0]; - nArgs.config = null; - break; - case 2: - if (YAHOO.lang.isObject(args[1]) && !args[1].tagName && !(args[1] instanceof String)) { - nArgs.id = null; - nArgs.container = args[0]; - nArgs.config = args[1]; - } else { - nArgs.id = args[0]; - nArgs.container = args[1]; - nArgs.config = null; - } - break; - default: // 3+ - nArgs.id = args[0]; - nArgs.container = args[1]; - nArgs.config = args[2]; - break; - } - } else { - } - return nArgs; - }, - - /** - * Initializes the Calendar widget. - * @method init - * - * @param {String} id optional The id of the table element that will represent the Calendar widget. As of 2.4.0, this argument is optional. - * @param {String | HTMLElement} container The id of the container div element that will wrap the Calendar table, or a reference to a DIV element which exists in the document. - * @param {Object} config optional The configuration object containing the initial configuration values for the Calendar. - */ - init : function(id, container, config) { - // Normalize 2.4.0, pre 2.4.0 args - var nArgs = this._parseArgs(arguments); - - id = nArgs.id; - container = nArgs.container; - config = nArgs.config; - - this.oDomContainer = YAHOO.util.Dom.get(container); - - if (!this.oDomContainer.id) { - this.oDomContainer.id = YAHOO.util.Dom.generateId(); - } - if (!id) { - id = this.oDomContainer.id + "_t"; - } - - this.id = id; - this.containerId = this.oDomContainer.id; - - this.initEvents(); - - this.today = new Date(); - YAHOO.widget.DateMath.clearTime(this.today); - - /** - * The Config object used to hold the configuration variables for the Calendar - * @property cfg - * @type YAHOO.util.Config - */ - this.cfg = new YAHOO.util.Config(this); - - /** - * The local object which contains the Calendar's options - * @property Options - * @type Object - */ - this.Options = {}; - - /** - * The local object which contains the Calendar's locale settings - * @property Locale - * @type Object - */ - this.Locale = {}; - - this.initStyles(); - - YAHOO.util.Dom.addClass(this.oDomContainer, this.Style.CSS_CONTAINER); - YAHOO.util.Dom.addClass(this.oDomContainer, this.Style.CSS_SINGLE); - - this.cellDates = []; - this.cells = []; - this.renderStack = []; - this._renderStack = []; - - this.setupConfig(); - - if (config) { - this.cfg.applyConfig(config, true); - } - - this.cfg.fireQueue(); - }, - - /** - * Default Config listener for the iframe property. If the iframe config property is set to true, - * renders the built-in IFRAME shim if the container is relatively or absolutely positioned. - * - * @method configIframe - */ - configIframe : function(type, args, obj) { - var useIframe = args[0]; - - if (!this.parent) { - if (YAHOO.util.Dom.inDocument(this.oDomContainer)) { - if (useIframe) { - var pos = YAHOO.util.Dom.getStyle(this.oDomContainer, "position"); - - if (pos == "absolute" || pos == "relative") { - - if (!YAHOO.util.Dom.inDocument(this.iframe)) { - this.iframe = document.createElement("iframe"); - this.iframe.src = "javascript:false;"; - - YAHOO.util.Dom.setStyle(this.iframe, "opacity", "0"); - - if (YAHOO.env.ua.ie && YAHOO.env.ua.ie <= 6) { - YAHOO.util.Dom.addClass(this.iframe, "fixedsize"); - } - - this.oDomContainer.insertBefore(this.iframe, this.oDomContainer.firstChild); - } - } - } else { - if (this.iframe) { - if (this.iframe.parentNode) { - this.iframe.parentNode.removeChild(this.iframe); - } - this.iframe = null; - } - } - } - } - }, - - /** - * Default handler for the "title" property - * @method configTitle - */ - configTitle : function(type, args, obj) { - var title = args[0]; - - // "" disables title bar - if (title) { - this.createTitleBar(title); - } else { - var close = this.cfg.getProperty(YAHOO.widget.Calendar._DEFAULT_CONFIG.CLOSE.key); - if (!close) { - this.removeTitleBar(); - } else { - this.createTitleBar(" "); - } - } - }, - - /** - * Default handler for the "close" property - * @method configClose - */ - configClose : function(type, args, obj) { - var close = args[0], - title = this.cfg.getProperty(YAHOO.widget.Calendar._DEFAULT_CONFIG.TITLE.key); - - if (close) { - if (!title) { - this.createTitleBar(" "); - } - this.createCloseButton(); - } else { - this.removeCloseButton(); - if (!title) { - this.removeTitleBar(); - } - } - }, - - /** - * Initializes Calendar's built-in CustomEvents - * @method initEvents - */ - initEvents : function() { - - var defEvents = YAHOO.widget.Calendar._EVENT_TYPES; - - /** - * Fired before a selection is made - * @event beforeSelectEvent - */ - this.beforeSelectEvent = new YAHOO.util.CustomEvent(defEvents.BEFORE_SELECT); - - /** - * Fired when a selection is made - * @event selectEvent - * @param {Array} Array of Date field arrays in the format [YYYY, MM, DD]. - */ - this.selectEvent = new YAHOO.util.CustomEvent(defEvents.SELECT); - - /** - * Fired before a selection is made - * @event beforeDeselectEvent - */ - this.beforeDeselectEvent = new YAHOO.util.CustomEvent(defEvents.BEFORE_DESELECT); - - /** - * Fired when a selection is made - * @event deselectEvent - * @param {Array} Array of Date field arrays in the format [YYYY, MM, DD]. - */ - this.deselectEvent = new YAHOO.util.CustomEvent(defEvents.DESELECT); - - /** - * Fired when the Calendar page is changed - * @event changePageEvent - */ - this.changePageEvent = new YAHOO.util.CustomEvent(defEvents.CHANGE_PAGE); - - /** - * Fired before the Calendar is rendered - * @event beforeRenderEvent - */ - this.beforeRenderEvent = new YAHOO.util.CustomEvent(defEvents.BEFORE_RENDER); - - /** - * Fired when the Calendar is rendered - * @event renderEvent - */ - this.renderEvent = new YAHOO.util.CustomEvent(defEvents.RENDER); - - /** - * Fired when the Calendar is reset - * @event resetEvent - */ - this.resetEvent = new YAHOO.util.CustomEvent(defEvents.RESET); - - /** - * Fired when the Calendar is cleared - * @event clearEvent - */ - this.clearEvent = new YAHOO.util.CustomEvent(defEvents.CLEAR); - - /** - * Fired just before the Calendar is to be shown - * @event beforeShowEvent - */ - this.beforeShowEvent = new YAHOO.util.CustomEvent(defEvents.BEFORE_SHOW); - - /** - * Fired after the Calendar is shown - * @event showEvent - */ - this.showEvent = new YAHOO.util.CustomEvent(defEvents.SHOW); - - /** - * Fired just before the Calendar is to be hidden - * @event beforeHideEvent - */ - this.beforeHideEvent = new YAHOO.util.CustomEvent(defEvents.BEFORE_HIDE); - - /** - * Fired after the Calendar is hidden - * @event hideEvent - */ - this.hideEvent = new YAHOO.util.CustomEvent(defEvents.HIDE); - - /** - * Fired just before the CalendarNavigator is to be shown - * @event beforeShowNavEvent - */ - this.beforeShowNavEvent = new YAHOO.util.CustomEvent(defEvents.BEFORE_SHOW_NAV); - - /** - * Fired after the CalendarNavigator is shown - * @event showNavEvent - */ - this.showNavEvent = new YAHOO.util.CustomEvent(defEvents.SHOW_NAV); - - /** - * Fired just before the CalendarNavigator is to be hidden - * @event beforeHideNavEvent - */ - this.beforeHideNavEvent = new YAHOO.util.CustomEvent(defEvents.BEFORE_HIDE_NAV); - - /** - * Fired after the CalendarNavigator is hidden - * @event hideNavEvent - */ - this.hideNavEvent = new YAHOO.util.CustomEvent(defEvents.HIDE_NAV); - - /** - * Fired just before the CalendarNavigator is to be rendered - * @event beforeRenderNavEvent - */ - this.beforeRenderNavEvent = new YAHOO.util.CustomEvent(defEvents.BEFORE_RENDER_NAV); - - /** - * Fired after the CalendarNavigator is rendered - * @event renderNavEvent - */ - this.renderNavEvent = new YAHOO.util.CustomEvent(defEvents.RENDER_NAV); - - this.beforeSelectEvent.subscribe(this.onBeforeSelect, this, true); - this.selectEvent.subscribe(this.onSelect, this, true); - this.beforeDeselectEvent.subscribe(this.onBeforeDeselect, this, true); - this.deselectEvent.subscribe(this.onDeselect, this, true); - this.changePageEvent.subscribe(this.onChangePage, this, true); - this.renderEvent.subscribe(this.onRender, this, true); - this.resetEvent.subscribe(this.onReset, this, true); - this.clearEvent.subscribe(this.onClear, this, true); - }, - - /** - * The default event function that is attached to a date link within a calendar cell - * when the calendar is rendered. - * @method doSelectCell - * @param {DOMEvent} e The event - * @param {Calendar} cal A reference to the calendar passed by the Event utility - */ - doSelectCell : function(e, cal) { - var cell,index,d,date; - - var target = YAHOO.util.Event.getTarget(e); - var tagName = target.tagName.toLowerCase(); - var defSelector = false; - - while (tagName != "td" && ! YAHOO.util.Dom.hasClass(target, cal.Style.CSS_CELL_SELECTABLE)) { - - if (!defSelector && tagName == "a" && YAHOO.util.Dom.hasClass(target, cal.Style.CSS_CELL_SELECTOR)) { - defSelector = true; - } - - target = target.parentNode; - tagName = target.tagName.toLowerCase(); - // TODO: No need to go all the way up to html. - if (tagName == "html") { - return; - } - } - - if (defSelector) { - // Stop link href navigation for default renderer - YAHOO.util.Event.preventDefault(e); - } - - cell = target; - - if (YAHOO.util.Dom.hasClass(cell, cal.Style.CSS_CELL_SELECTABLE)) { - index = cell.id.split("cell")[1]; - d = cal.cellDates[index]; - date = YAHOO.widget.DateMath.getDate(d[0],d[1]-1,d[2]); - - var link; - - if (cal.Options.MULTI_SELECT) { - link = cell.getElementsByTagName("a")[0]; - if (link) { - link.blur(); - } - - var cellDate = cal.cellDates[index]; - var cellDateIndex = cal._indexOfSelectedFieldArray(cellDate); - - if (cellDateIndex > -1) { - cal.deselectCell(index); - } else { - cal.selectCell(index); - } - - } else { - link = cell.getElementsByTagName("a")[0]; - if (link) { - link.blur(); - } - cal.selectCell(index); - } - } - }, - - /** - * The event that is executed when the user hovers over a cell - * @method doCellMouseOver - * @param {DOMEvent} e The event - * @param {Calendar} cal A reference to the calendar passed by the Event utility - */ - doCellMouseOver : function(e, cal) { - var target; - if (e) { - target = YAHOO.util.Event.getTarget(e); - } else { - target = this; - } - - while (target.tagName && target.tagName.toLowerCase() != "td") { - target = target.parentNode; - if (!target.tagName || target.tagName.toLowerCase() == "html") { - return; - } - } - - if (YAHOO.util.Dom.hasClass(target, cal.Style.CSS_CELL_SELECTABLE)) { - YAHOO.util.Dom.addClass(target, cal.Style.CSS_CELL_HOVER); - } - }, - - /** - * The event that is executed when the user moves the mouse out of a cell - * @method doCellMouseOut - * @param {DOMEvent} e The event - * @param {Calendar} cal A reference to the calendar passed by the Event utility - */ - doCellMouseOut : function(e, cal) { - var target; - if (e) { - target = YAHOO.util.Event.getTarget(e); - } else { - target = this; - } - - while (target.tagName && target.tagName.toLowerCase() != "td") { - target = target.parentNode; - if (!target.tagName || target.tagName.toLowerCase() == "html") { - return; - } - } - - if (YAHOO.util.Dom.hasClass(target, cal.Style.CSS_CELL_SELECTABLE)) { - YAHOO.util.Dom.removeClass(target, cal.Style.CSS_CELL_HOVER); - } - }, - - setupConfig : function() { - - var defCfg = YAHOO.widget.Calendar._DEFAULT_CONFIG; - - /** - * The month/year representing the current visible Calendar date (mm/yyyy) - * @config pagedate - * @type String - * @default today's date - */ - this.cfg.addProperty(defCfg.PAGEDATE.key, { value:new Date(), handler:this.configPageDate } ); - - /** - * The date or range of dates representing the current Calendar selection - * @config selected - * @type String - * @default [] - */ - this.cfg.addProperty(defCfg.SELECTED.key, { value:[], handler:this.configSelected } ); - - /** - * The title to display above the Calendar's month header - * @config title - * @type String - * @default "" - */ - this.cfg.addProperty(defCfg.TITLE.key, { value:defCfg.TITLE.value, handler:this.configTitle } ); - - /** - * Whether or not a close button should be displayed for this Calendar - * @config close - * @type Boolean - * @default false - */ - this.cfg.addProperty(defCfg.CLOSE.key, { value:defCfg.CLOSE.value, handler:this.configClose } ); - - /** - * Whether or not an iframe shim should be placed under the Calendar to prevent select boxes from bleeding through in Internet Explorer 6 and below. - * This property is enabled by default for IE6 and below. It is disabled by default for other browsers for performance reasons, but can be - * enabled if required. - * - * @config iframe - * @type Boolean - * @default true for IE6 and below, false for all other browsers - */ - this.cfg.addProperty(defCfg.IFRAME.key, { value:defCfg.IFRAME.value, handler:this.configIframe, validator:this.cfg.checkBoolean } ); - - /** - * The minimum selectable date in the current Calendar (mm/dd/yyyy) - * @config mindate - * @type String - * @default null - */ - this.cfg.addProperty(defCfg.MINDATE.key, { value:defCfg.MINDATE.value, handler:this.configMinDate } ); - - /** - * The maximum selectable date in the current Calendar (mm/dd/yyyy) - * @config maxdate - * @type String - * @default null - */ - this.cfg.addProperty(defCfg.MAXDATE.key, { value:defCfg.MAXDATE.value, handler:this.configMaxDate } ); - - - // Options properties - - /** - * True if the Calendar should allow multiple selections. False by default. - * @config MULTI_SELECT - * @type Boolean - * @default false - */ - this.cfg.addProperty(defCfg.MULTI_SELECT.key, { value:defCfg.MULTI_SELECT.value, handler:this.configOptions, validator:this.cfg.checkBoolean } ); - - /** - * The weekday the week begins on. Default is 0 (Sunday). - * @config START_WEEKDAY - * @type number - * @default 0 - */ - this.cfg.addProperty(defCfg.START_WEEKDAY.key, { value:defCfg.START_WEEKDAY.value, handler:this.configOptions, validator:this.cfg.checkNumber } ); - - /** - * True if the Calendar should show weekday labels. True by default. - * @config SHOW_WEEKDAYS - * @type Boolean - * @default true - */ - this.cfg.addProperty(defCfg.SHOW_WEEKDAYS.key, { value:defCfg.SHOW_WEEKDAYS.value, handler:this.configOptions, validator:this.cfg.checkBoolean } ); - - /** - * True if the Calendar should show week row headers. False by default. - * @config SHOW_WEEK_HEADER - * @type Boolean - * @default false - */ - this.cfg.addProperty(defCfg.SHOW_WEEK_HEADER.key, { value:defCfg.SHOW_WEEK_HEADER.value, handler:this.configOptions, validator:this.cfg.checkBoolean } ); - - /** - * True if the Calendar should show week row footers. False by default. - * @config SHOW_WEEK_FOOTER - * @type Boolean - * @default false - */ - this.cfg.addProperty(defCfg.SHOW_WEEK_FOOTER.key,{ value:defCfg.SHOW_WEEK_FOOTER.value, handler:this.configOptions, validator:this.cfg.checkBoolean } ); - - /** - * True if the Calendar should suppress weeks that are not a part of the current month. False by default. - * @config HIDE_BLANK_WEEKS - * @type Boolean - * @default false - */ - this.cfg.addProperty(defCfg.HIDE_BLANK_WEEKS.key, { value:defCfg.HIDE_BLANK_WEEKS.value, handler:this.configOptions, validator:this.cfg.checkBoolean } ); - - /** - * The image that should be used for the left navigation arrow. - * @config NAV_ARROW_LEFT - * @type String - * @deprecated You can customize the image by overriding the default CSS class for the left arrow - "calnavleft" - * @default null - */ - this.cfg.addProperty(defCfg.NAV_ARROW_LEFT.key, { value:defCfg.NAV_ARROW_LEFT.value, handler:this.configOptions } ); - - /** - * The image that should be used for the right navigation arrow. - * @config NAV_ARROW_RIGHT - * @type String - * @deprecated You can customize the image by overriding the default CSS class for the right arrow - "calnavright" - * @default null - */ - this.cfg.addProperty(defCfg.NAV_ARROW_RIGHT.key, { value:defCfg.NAV_ARROW_RIGHT.value, handler:this.configOptions } ); - - // Locale properties - - /** - * The short month labels for the current locale. - * @config MONTHS_SHORT - * @type String[] - * @default ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] - */ - this.cfg.addProperty(defCfg.MONTHS_SHORT.key, { value:defCfg.MONTHS_SHORT.value, handler:this.configLocale } ); - - /** - * The long month labels for the current locale. - * @config MONTHS_LONG - * @type String[] - * @default ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" - */ - this.cfg.addProperty(defCfg.MONTHS_LONG.key, { value:defCfg.MONTHS_LONG.value, handler:this.configLocale } ); - - /** - * The 1-character weekday labels for the current locale. - * @config WEEKDAYS_1CHAR - * @type String[] - * @default ["S", "M", "T", "W", "T", "F", "S"] - */ - this.cfg.addProperty(defCfg.WEEKDAYS_1CHAR.key, { value:defCfg.WEEKDAYS_1CHAR.value, handler:this.configLocale } ); - - /** - * The short weekday labels for the current locale. - * @config WEEKDAYS_SHORT - * @type String[] - * @default ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"] - */ - this.cfg.addProperty(defCfg.WEEKDAYS_SHORT.key, { value:defCfg.WEEKDAYS_SHORT.value, handler:this.configLocale } ); - - /** - * The medium weekday labels for the current locale. - * @config WEEKDAYS_MEDIUM - * @type String[] - * @default ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"] - */ - this.cfg.addProperty(defCfg.WEEKDAYS_MEDIUM.key, { value:defCfg.WEEKDAYS_MEDIUM.value, handler:this.configLocale } ); - - /** - * The long weekday labels for the current locale. - * @config WEEKDAYS_LONG - * @type String[] - * @default ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"] - */ - this.cfg.addProperty(defCfg.WEEKDAYS_LONG.key, { value:defCfg.WEEKDAYS_LONG.value, handler:this.configLocale } ); - - /** - * Refreshes the locale values used to build the Calendar. - * @method refreshLocale - * @private - */ - var refreshLocale = function() { - this.cfg.refireEvent(defCfg.LOCALE_MONTHS.key); - this.cfg.refireEvent(defCfg.LOCALE_WEEKDAYS.key); - }; - - this.cfg.subscribeToConfigEvent(defCfg.START_WEEKDAY.key, refreshLocale, this, true); - this.cfg.subscribeToConfigEvent(defCfg.MONTHS_SHORT.key, refreshLocale, this, true); - this.cfg.subscribeToConfigEvent(defCfg.MONTHS_LONG.key, refreshLocale, this, true); - this.cfg.subscribeToConfigEvent(defCfg.WEEKDAYS_1CHAR.key, refreshLocale, this, true); - this.cfg.subscribeToConfigEvent(defCfg.WEEKDAYS_SHORT.key, refreshLocale, this, true); - this.cfg.subscribeToConfigEvent(defCfg.WEEKDAYS_MEDIUM.key, refreshLocale, this, true); - this.cfg.subscribeToConfigEvent(defCfg.WEEKDAYS_LONG.key, refreshLocale, this, true); - - /** - * The setting that determines which length of month labels should be used. Possible values are "short" and "long". - * @config LOCALE_MONTHS - * @type String - * @default "long" - */ - this.cfg.addProperty(defCfg.LOCALE_MONTHS.key, { value:defCfg.LOCALE_MONTHS.value, handler:this.configLocaleValues } ); - - /** - * The setting that determines which length of weekday labels should be used. Possible values are "1char", "short", "medium", and "long". - * @config LOCALE_WEEKDAYS - * @type String - * @default "short" - */ - this.cfg.addProperty(defCfg.LOCALE_WEEKDAYS.key, { value:defCfg.LOCALE_WEEKDAYS.value, handler:this.configLocaleValues } ); - - /** - * The value used to delimit individual dates in a date string passed to various Calendar functions. - * @config DATE_DELIMITER - * @type String - * @default "," - */ - this.cfg.addProperty(defCfg.DATE_DELIMITER.key, { value:defCfg.DATE_DELIMITER.value, handler:this.configLocale } ); - - /** - * The value used to delimit date fields in a date string passed to various Calendar functions. - * @config DATE_FIELD_DELIMITER - * @type String - * @default "/" - */ - this.cfg.addProperty(defCfg.DATE_FIELD_DELIMITER.key, { value:defCfg.DATE_FIELD_DELIMITER.value, handler:this.configLocale } ); - - /** - * The value used to delimit date ranges in a date string passed to various Calendar functions. - * @config DATE_RANGE_DELIMITER - * @type String - * @default "-" - */ - this.cfg.addProperty(defCfg.DATE_RANGE_DELIMITER.key, { value:defCfg.DATE_RANGE_DELIMITER.value, handler:this.configLocale } ); - - /** - * The position of the month in a month/year date string - * @config MY_MONTH_POSITION - * @type Number - * @default 1 - */ - this.cfg.addProperty(defCfg.MY_MONTH_POSITION.key, { value:defCfg.MY_MONTH_POSITION.value, handler:this.configLocale, validator:this.cfg.checkNumber } ); - - /** - * The position of the year in a month/year date string - * @config MY_YEAR_POSITION - * @type Number - * @default 2 - */ - this.cfg.addProperty(defCfg.MY_YEAR_POSITION.key, { value:defCfg.MY_YEAR_POSITION.value, handler:this.configLocale, validator:this.cfg.checkNumber } ); - - /** - * The position of the month in a month/day date string - * @config MD_MONTH_POSITION - * @type Number - * @default 1 - */ - this.cfg.addProperty(defCfg.MD_MONTH_POSITION.key, { value:defCfg.MD_MONTH_POSITION.value, handler:this.configLocale, validator:this.cfg.checkNumber } ); - - /** - * The position of the day in a month/year date string - * @config MD_DAY_POSITION - * @type Number - * @default 2 - */ - this.cfg.addProperty(defCfg.MD_DAY_POSITION.key, { value:defCfg.MD_DAY_POSITION.value, handler:this.configLocale, validator:this.cfg.checkNumber } ); - - /** - * The position of the month in a month/day/year date string - * @config MDY_MONTH_POSITION - * @type Number - * @default 1 - */ - this.cfg.addProperty(defCfg.MDY_MONTH_POSITION.key, { value:defCfg.MDY_MONTH_POSITION.value, handler:this.configLocale, validator:this.cfg.checkNumber } ); - - /** - * The position of the day in a month/day/year date string - * @config MDY_DAY_POSITION - * @type Number - * @default 2 - */ - this.cfg.addProperty(defCfg.MDY_DAY_POSITION.key, { value:defCfg.MDY_DAY_POSITION.value, handler:this.configLocale, validator:this.cfg.checkNumber } ); - - /** - * The position of the year in a month/day/year date string - * @config MDY_YEAR_POSITION - * @type Number - * @default 3 - */ - this.cfg.addProperty(defCfg.MDY_YEAR_POSITION.key, { value:defCfg.MDY_YEAR_POSITION.value, handler:this.configLocale, validator:this.cfg.checkNumber } ); - - /** - * The position of the month in the month year label string used as the Calendar header - * @config MY_LABEL_MONTH_POSITION - * @type Number - * @default 1 - */ - this.cfg.addProperty(defCfg.MY_LABEL_MONTH_POSITION.key, { value:defCfg.MY_LABEL_MONTH_POSITION.value, handler:this.configLocale, validator:this.cfg.checkNumber } ); - - /** - * The position of the year in the month year label string used as the Calendar header - * @config MY_LABEL_YEAR_POSITION - * @type Number - * @default 2 - */ - this.cfg.addProperty(defCfg.MY_LABEL_YEAR_POSITION.key, { value:defCfg.MY_LABEL_YEAR_POSITION.value, handler:this.configLocale, validator:this.cfg.checkNumber } ); - - /** - * The suffix used after the month when rendering the Calendar header - * @config MY_LABEL_MONTH_SUFFIX - * @type String - * @default " " - */ - this.cfg.addProperty(defCfg.MY_LABEL_MONTH_SUFFIX.key, { value:defCfg.MY_LABEL_MONTH_SUFFIX.value, handler:this.configLocale } ); - - /** - * The suffix used after the year when rendering the Calendar header - * @config MY_LABEL_YEAR_SUFFIX - * @type String - * @default "" - */ - this.cfg.addProperty(defCfg.MY_LABEL_YEAR_SUFFIX.key, { value:defCfg.MY_LABEL_YEAR_SUFFIX.value, handler:this.configLocale } ); - - /** - * Configuration for the Month/Year CalendarNavigator UI which allows the user to jump directly to a - * specific Month/Year without having to scroll sequentially through months. - * <p> - * Setting this property to null (default value) or false, will disable the CalendarNavigator UI. - * </p> - * <p> - * Setting this property to true will enable the CalendarNavigatior UI with the default CalendarNavigator configuration values. - * </p> - * <p> - * This property can also be set to an object literal containing configuration properties for the CalendarNavigator UI. - * The configuration object expects the the following case-sensitive properties, with the "strings" property being a nested object. - * Any properties which are not provided will use the default values (defined in the CalendarNavigator class). - * </p> - * <dl> - * <dt>strings</dt> - * <dd><em>Object</em> : An object with the properties shown below, defining the string labels to use in the Navigator's UI - * <dl> - * <dt>month</dt><dd><em>String</em> : The string to use for the month label. Defaults to "Month".</dd> - * <dt>year</dt><dd><em>String</em> : The string to use for the year label. Defaults to "Year".</dd> - * <dt>submit</dt><dd><em>String</em> : The string to use for the submit button label. Defaults to "Okay".</dd> - * <dt>cancel</dt><dd><em>String</em> : The string to use for the cancel button label. Defaults to "Cancel".</dd> - * <dt>invalidYear</dt><dd><em>String</em> : The string to use for invalid year values. Defaults to "Year needs to be a number".</dd> - * </dl> - * </dd> - * <dt>monthFormat</dt><dd><em>String</em> : The month format to use. Either YAHOO.widget.Calendar.LONG, or YAHOO.widget.Calendar.SHORT. Defaults to YAHOO.widget.Calendar.LONG</dd> - * <dt>initialFocus</dt><dd><em>String</em> : Either "year" or "month" specifying which input control should get initial focus. Defaults to "year"</dd> - * </dl> - * <p>E.g.</p> - * <pre> - * var navConfig = { - * strings: { - * month:"Calendar Month", - * year:"Calendar Year", - * submit: "Submit", - * cancel: "Cancel", - * invalidYear: "Please enter a valid year" - * }, - * monthFormat: YAHOO.widget.Calendar.SHORT, - * initialFocus: "month" - * } - * </pre> - * @config navigator - * @type {Object|Boolean} - * @default null - */ - this.cfg.addProperty(defCfg.NAV.key, { value:defCfg.NAV.value, handler:this.configNavigator } ); - }, - - /** - * The default handler for the "pagedate" property - * @method configPageDate - */ - configPageDate : function(type, args, obj) { - this.cfg.setProperty(YAHOO.widget.Calendar._DEFAULT_CONFIG.PAGEDATE.key, this._parsePageDate(args[0]), true); - }, - - /** - * The default handler for the "mindate" property - * @method configMinDate - */ - configMinDate : function(type, args, obj) { - var val = args[0]; - if (YAHOO.lang.isString(val)) { - val = this._parseDate(val); - this.cfg.setProperty(YAHOO.widget.Calendar._DEFAULT_CONFIG.MINDATE.key, YAHOO.widget.DateMath.getDate(val[0],(val[1]-1),val[2])); - } - }, - - /** - * The default handler for the "maxdate" property - * @method configMaxDate - */ - configMaxDate : function(type, args, obj) { - var val = args[0]; - if (YAHOO.lang.isString(val)) { - val = this._parseDate(val); - this.cfg.setProperty(YAHOO.widget.Calendar._DEFAULT_CONFIG.MAXDATE.key, YAHOO.widget.DateMath.getDate(val[0],(val[1]-1),val[2])); - } - }, - - /** - * The default handler for the "selected" property - * @method configSelected - */ - configSelected : function(type, args, obj) { - var selected = args[0]; - var cfgSelected = YAHOO.widget.Calendar._DEFAULT_CONFIG.SELECTED.key; - - if (selected) { - if (YAHOO.lang.isString(selected)) { - this.cfg.setProperty(cfgSelected, this._parseDates(selected), true); - } - } - if (! this._selectedDates) { - this._selectedDates = this.cfg.getProperty(cfgSelected); - } - }, - - /** - * The default handler for all configuration options properties - * @method configOptions - */ - configOptions : function(type, args, obj) { - this.Options[type.toUpperCase()] = args[0]; - }, - - /** - * The default handler for all configuration locale properties - * @method configLocale - */ - configLocale : function(type, args, obj) { - var defCfg = YAHOO.widget.Calendar._DEFAULT_CONFIG; - this.Locale[type.toUpperCase()] = args[0]; - - this.cfg.refireEvent(defCfg.LOCALE_MONTHS.key); - this.cfg.refireEvent(defCfg.LOCALE_WEEKDAYS.key); - }, - - /** - * The default handler for all configuration locale field length properties - * @method configLocaleValues - */ - configLocaleValues : function(type, args, obj) { - var defCfg = YAHOO.widget.Calendar._DEFAULT_CONFIG; - - type = type.toLowerCase(); - var val = args[0]; - - switch (type) { - case defCfg.LOCALE_MONTHS.key: - switch (val) { - case YAHOO.widget.Calendar.SHORT: - this.Locale.LOCALE_MONTHS = this.cfg.getProperty(defCfg.MONTHS_SHORT.key).concat(); - break; - case YAHOO.widget.Calendar.LONG: - this.Locale.LOCALE_MONTHS = this.cfg.getProperty(defCfg.MONTHS_LONG.key).concat(); - break; - } - break; - case defCfg.LOCALE_WEEKDAYS.key: - switch (val) { - case YAHOO.widget.Calendar.ONE_CHAR: - this.Locale.LOCALE_WEEKDAYS = this.cfg.getProperty(defCfg.WEEKDAYS_1CHAR.key).concat(); - break; - case YAHOO.widget.Calendar.SHORT: - this.Locale.LOCALE_WEEKDAYS = this.cfg.getProperty(defCfg.WEEKDAYS_SHORT.key).concat(); - break; - case YAHOO.widget.Calendar.MEDIUM: - this.Locale.LOCALE_WEEKDAYS = this.cfg.getProperty(defCfg.WEEKDAYS_MEDIUM.key).concat(); - break; - case YAHOO.widget.Calendar.LONG: - this.Locale.LOCALE_WEEKDAYS = this.cfg.getProperty(defCfg.WEEKDAYS_LONG.key).concat(); - break; - } - - var START_WEEKDAY = this.cfg.getProperty(defCfg.START_WEEKDAY.key); - - if (START_WEEKDAY > 0) { - for (var w=0;w<START_WEEKDAY;++w) { - this.Locale.LOCALE_WEEKDAYS.push(this.Locale.LOCALE_WEEKDAYS.shift()); - } - } - break; - } - }, - - /** - * The default handler for the "navigator" property - * @method configNavigator - */ - configNavigator : function(type, args, obj) { - var val = args[0]; - if (YAHOO.widget.CalendarNavigator && (val === true || YAHOO.lang.isObject(val))) { - if (!this.oNavigator) { - this.oNavigator = new YAHOO.widget.CalendarNavigator(this); - // Cleanup DOM Refs/Events before innerHTML is removed. - function erase() { - if (!this.pages) { - this.oNavigator.erase(); - } - } - this.beforeRenderEvent.subscribe(erase, this, true); - } - } else { - if (this.oNavigator) { - this.oNavigator.destroy(); - this.oNavigator = null; - } - } - }, - - /** - * Defines the style constants for the Calendar - * @method initStyles - */ - initStyles : function() { - - var defStyle = YAHOO.widget.Calendar._STYLES; - - this.Style = { - /** - * @property Style.CSS_ROW_HEADER - */ - CSS_ROW_HEADER: defStyle.CSS_ROW_HEADER, - /** - * @property Style.CSS_ROW_FOOTER - */ - CSS_ROW_FOOTER: defStyle.CSS_ROW_FOOTER, - /** - * @property Style.CSS_CELL - */ - CSS_CELL : defStyle.CSS_CELL, - /** - * @property Style.CSS_CELL_SELECTOR - */ - CSS_CELL_SELECTOR : defStyle.CSS_CELL_SELECTOR, - /** - * @property Style.CSS_CELL_SELECTED - */ - CSS_CELL_SELECTED : defStyle.CSS_CELL_SELECTED, - /** - * @property Style.CSS_CELL_SELECTABLE - */ - CSS_CELL_SELECTABLE : defStyle.CSS_CELL_SELECTABLE, - /** - * @property Style.CSS_CELL_RESTRICTED - */ - CSS_CELL_RESTRICTED : defStyle.CSS_CELL_RESTRICTED, - /** - * @property Style.CSS_CELL_TODAY - */ - CSS_CELL_TODAY : defStyle.CSS_CELL_TODAY, - /** - * @property Style.CSS_CELL_OOM - */ - CSS_CELL_OOM : defStyle.CSS_CELL_OOM, - /** - * @property Style.CSS_CELL_OOB - */ - CSS_CELL_OOB : defStyle.CSS_CELL_OOB, - /** - * @property Style.CSS_HEADER - */ - CSS_HEADER : defStyle.CSS_HEADER, - /** - * @property Style.CSS_HEADER_TEXT - */ - CSS_HEADER_TEXT : defStyle.CSS_HEADER_TEXT, - /** - * @property Style.CSS_BODY - */ - CSS_BODY : defStyle.CSS_BODY, - /** - * @property Style.CSS_WEEKDAY_CELL - */ - CSS_WEEKDAY_CELL : defStyle.CSS_WEEKDAY_CELL, - /** - * @property Style.CSS_WEEKDAY_ROW - */ - CSS_WEEKDAY_ROW : defStyle.CSS_WEEKDAY_ROW, - /** - * @property Style.CSS_FOOTER - */ - CSS_FOOTER : defStyle.CSS_FOOTER, - /** - * @property Style.CSS_CALENDAR - */ - CSS_CALENDAR : defStyle.CSS_CALENDAR, - /** - * @property Style.CSS_SINGLE - */ - CSS_SINGLE : defStyle.CSS_SINGLE, - /** - * @property Style.CSS_CONTAINER - */ - CSS_CONTAINER : defStyle.CSS_CONTAINER, - /** - * @property Style.CSS_NAV_LEFT - */ - CSS_NAV_LEFT : defStyle.CSS_NAV_LEFT, - /** - * @property Style.CSS_NAV_RIGHT - */ - CSS_NAV_RIGHT : defStyle.CSS_NAV_RIGHT, - /** - * @property Style.CSS_NAV - */ - CSS_NAV : defStyle.CSS_NAV, - /** - * @property Style.CSS_CLOSE - */ - CSS_CLOSE : defStyle.CSS_CLOSE, - /** - * @property Style.CSS_CELL_TOP - */ - CSS_CELL_TOP : defStyle.CSS_CELL_TOP, - /** - * @property Style.CSS_CELL_LEFT - */ - CSS_CELL_LEFT : defStyle.CSS_CELL_LEFT, - /** - * @property Style.CSS_CELL_RIGHT - */ - CSS_CELL_RIGHT : defStyle.CSS_CELL_RIGHT, - /** - * @property Style.CSS_CELL_BOTTOM - */ - CSS_CELL_BOTTOM : defStyle.CSS_CELL_BOTTOM, - /** - * @property Style.CSS_CELL_HOVER - */ - CSS_CELL_HOVER : defStyle.CSS_CELL_HOVER, - /** - * @property Style.CSS_CELL_HIGHLIGHT1 - */ - CSS_CELL_HIGHLIGHT1 : defStyle.CSS_CELL_HIGHLIGHT1, - /** - * @property Style.CSS_CELL_HIGHLIGHT2 - */ - CSS_CELL_HIGHLIGHT2 : defStyle.CSS_CELL_HIGHLIGHT2, - /** - * @property Style.CSS_CELL_HIGHLIGHT3 - */ - CSS_CELL_HIGHLIGHT3 : defStyle.CSS_CELL_HIGHLIGHT3, - /** - * @property Style.CSS_CELL_HIGHLIGHT4 - */ - CSS_CELL_HIGHLIGHT4 : defStyle.CSS_CELL_HIGHLIGHT4 - }; - }, - - /** - * Builds the date label that will be displayed in the calendar header or - * footer, depending on configuration. - * @method buildMonthLabel - * @return {String} The formatted calendar month label - */ - buildMonthLabel : function() { - var pageDate = this.cfg.getProperty(YAHOO.widget.Calendar._DEFAULT_CONFIG.PAGEDATE.key); - - var monthLabel = this.Locale.LOCALE_MONTHS[pageDate.getMonth()] + this.Locale.MY_LABEL_MONTH_SUFFIX; - var yearLabel = pageDate.getFullYear() + this.Locale.MY_LABEL_YEAR_SUFFIX; - - if (this.Locale.MY_LABEL_MONTH_POSITION == 2 || this.Locale.MY_LABEL_YEAR_POSITION == 1) { - return yearLabel + monthLabel; - } else { - return monthLabel + yearLabel; - } - }, - - /** - * Builds the date digit that will be displayed in calendar cells - * @method buildDayLabel - * @param {Date} workingDate The current working date - * @return {String} The formatted day label - */ - buildDayLabel : function(workingDate) { - return workingDate.getDate(); - }, - - /** - * Creates the title bar element and adds it to Calendar container DIV - * - * @method createTitleBar - * @param {String} strTitle The title to display in the title bar - * @return The title bar element - */ - createTitleBar : function(strTitle) { - var tDiv = YAHOO.util.Dom.getElementsByClassName(YAHOO.widget.CalendarGroup.CSS_2UPTITLE, "div", this.oDomContainer)[0] || document.createElement("div"); - tDiv.className = YAHOO.widget.CalendarGroup.CSS_2UPTITLE; - tDiv.innerHTML = strTitle; - this.oDomContainer.insertBefore(tDiv, this.oDomContainer.firstChild); - - YAHOO.util.Dom.addClass(this.oDomContainer, "withtitle"); - - return tDiv; - }, - - /** - * Removes the title bar element from the DOM - * - * @method removeTitleBar - */ - removeTitleBar : function() { - var tDiv = YAHOO.util.Dom.getElementsByClassName(YAHOO.widget.CalendarGroup.CSS_2UPTITLE, "div", this.oDomContainer)[0] || null; - if (tDiv) { - YAHOO.util.Event.purgeElement(tDiv); - this.oDomContainer.removeChild(tDiv); - } - YAHOO.util.Dom.removeClass(this.oDomContainer, "withtitle"); - }, - - /** - * Creates the close button HTML element and adds it to Calendar container DIV - * - * @method createCloseButton - * @return The close HTML element created - */ - createCloseButton : function() { - var Dom = YAHOO.util.Dom, - Event = YAHOO.util.Event, - cssClose = YAHOO.widget.CalendarGroup.CSS_2UPCLOSE, - DEPR_CLOSE_PATH = "us/my/bn/x_d.gif"; - - var lnk = Dom.getElementsByClassName("link-close", "a", this.oDomContainer)[0]; - - if (!lnk) { - lnk = document.createElement("a"); - Event.addListener(lnk, "click", function(e, cal) { - cal.hide(); - Event.preventDefault(e); - }, this); - } - - lnk.href = "#"; - lnk.className = "link-close"; - - if (YAHOO.widget.Calendar.IMG_ROOT !== null) { - var img = Dom.getElementsByClassName(cssClose, "img", lnk)[0] || document.createElement("img"); - img.src = YAHOO.widget.Calendar.IMG_ROOT + DEPR_CLOSE_PATH; - img.className = cssClose; - lnk.appendChild(img); - } else { - lnk.innerHTML = '<span class="' + cssClose + ' ' + this.Style.CSS_CLOSE + '"></span>'; - } - this.oDomContainer.appendChild(lnk); - - return lnk; - }, - - /** - * Removes the close button HTML element from the DOM - * - * @method removeCloseButton - */ - removeCloseButton : function() { - var btn = YAHOO.util.Dom.getElementsByClassName("link-close", "a", this.oDomContainer)[0] || null; - if (btn) { - YAHOO.util.Event.purgeElement(btn); - this.oDomContainer.removeChild(btn); - } - }, - - /** - * Renders the calendar header. - * @method renderHeader - * @param {Array} html The current working HTML array - * @return {Array} The current working HTML array - */ - renderHeader : function(html) { - var colSpan = 7; - - var DEPR_NAV_LEFT = "us/tr/callt.gif"; - var DEPR_NAV_RIGHT = "us/tr/calrt.gif"; - var defCfg = YAHOO.widget.Calendar._DEFAULT_CONFIG; - - if (this.cfg.getProperty(defCfg.SHOW_WEEK_HEADER.key)) { - colSpan += 1; - } - - if (this.cfg.getProperty(defCfg.SHOW_WEEK_FOOTER.key)) { - colSpan += 1; - } - - html[html.length] = "<thead>"; - html[html.length] = "<tr>"; - html[html.length] = '<th colspan="' + colSpan + '" class="' + this.Style.CSS_HEADER_TEXT + '">'; - html[html.length] = '<div class="' + this.Style.CSS_HEADER + '">'; - - var renderLeft, renderRight = false; - - if (this.parent) { - if (this.index === 0) { - renderLeft = true; - } - if (this.index == (this.parent.cfg.getProperty("pages") -1)) { - renderRight = true; - } - } else { - renderLeft = true; - renderRight = true; - } - - if (renderLeft) { - var leftArrow = this.cfg.getProperty(defCfg.NAV_ARROW_LEFT.key); - // Check for deprecated customization - If someone set IMG_ROOT, but didn't set NAV_ARROW_LEFT, then set NAV_ARROW_LEFT to the old deprecated value - if (leftArrow === null && YAHOO.widget.Calendar.IMG_ROOT !== null) { - leftArrow = YAHOO.widget.Calendar.IMG_ROOT + DEPR_NAV_LEFT; - } - var leftStyle = (leftArrow === null) ? "" : ' style="background-image:url(' + leftArrow + ')"'; - html[html.length] = '<a class="' + this.Style.CSS_NAV_LEFT + '"' + leftStyle + ' > </a>'; - } - - var lbl = this.buildMonthLabel(); - var cal = this.parent || this; - if (cal.cfg.getProperty("navigator")) { - lbl = "<a class=\"" + this.Style.CSS_NAV + "\" href=\"#\">" + lbl + "</a>"; - } - html[html.length] = lbl; - - if (renderRight) { - var rightArrow = this.cfg.getProperty(defCfg.NAV_ARROW_RIGHT.key); - if (rightArrow === null && YAHOO.widget.Calendar.IMG_ROOT !== null) { - rightArrow = YAHOO.widget.Calendar.IMG_ROOT + DEPR_NAV_RIGHT; - } - var rightStyle = (rightArrow === null) ? "" : ' style="background-image:url(' + rightArrow + ')"'; - html[html.length] = '<a class="' + this.Style.CSS_NAV_RIGHT + '"' + rightStyle + ' > </a>'; - } - - html[html.length] = '</div>\n</th>\n</tr>'; - - if (this.cfg.getProperty(defCfg.SHOW_WEEKDAYS.key)) { - html = this.buildWeekdays(html); - } - - html[html.length] = '</thead>'; - - return html; - }, - - /** - * Renders the Calendar's weekday headers. - * @method buildWeekdays - * @param {Array} html The current working HTML array - * @return {Array} The current working HTML array - */ - buildWeekdays : function(html) { - - var defCfg = YAHOO.widget.Calendar._DEFAULT_CONFIG; - - html[html.length] = '<tr class="' + this.Style.CSS_WEEKDAY_ROW + '">'; - - if (this.cfg.getProperty(defCfg.SHOW_WEEK_HEADER.key)) { - html[html.length] = '<th> </th>'; - } - - for(var i=0;i<this.Locale.LOCALE_WEEKDAYS.length;++i) { - html[html.length] = '<th class="calweekdaycell">' + this.Locale.LOCALE_WEEKDAYS[i] + '</th>'; - } - - if (this.cfg.getProperty(defCfg.SHOW_WEEK_FOOTER.key)) { - html[html.length] = '<th> </th>'; - } - - html[html.length] = '</tr>'; - - return html; - }, - - /** - * Renders the calendar body. - * @method renderBody - * @param {Date} workingDate The current working Date being used for the render process - * @param {Array} html The current working HTML array - * @return {Array} The current working HTML array - */ - renderBody : function(workingDate, html) { - var defCfg = YAHOO.widget.Calendar._DEFAULT_CONFIG; - - var startDay = this.cfg.getProperty(defCfg.START_WEEKDAY.key); - - this.preMonthDays = workingDate.getDay(); - if (startDay > 0) { - this.preMonthDays -= startDay; - } - if (this.preMonthDays < 0) { - this.preMonthDays += 7; - } - - this.monthDays = YAHOO.widget.DateMath.findMonthEnd(workingDate).getDate(); - this.postMonthDays = YAHOO.widget.Calendar.DISPLAY_DAYS-this.preMonthDays-this.monthDays; - - workingDate = YAHOO.widget.DateMath.subtract(workingDate, YAHOO.widget.DateMath.DAY, this.preMonthDays); - - var weekNum,weekClass; - var weekPrefix = "w"; - var cellPrefix = "_cell"; - var workingDayPrefix = "wd"; - var dayPrefix = "d"; - - var cellRenderers; - var renderer; - - var todayYear = this.today.getFullYear(); - var todayMonth = this.today.getMonth(); - var todayDate = this.today.getDate(); - - var useDate = this.cfg.getProperty(defCfg.PAGEDATE.key); - var hideBlankWeeks = this.cfg.getProperty(defCfg.HIDE_BLANK_WEEKS.key); - var showWeekFooter = this.cfg.getProperty(defCfg.SHOW_WEEK_FOOTER.key); - var showWeekHeader = this.cfg.getProperty(defCfg.SHOW_WEEK_HEADER.key); - var mindate = this.cfg.getProperty(defCfg.MINDATE.key); - var maxdate = this.cfg.getProperty(defCfg.MAXDATE.key); - - if (mindate) { - mindate = YAHOO.widget.DateMath.clearTime(mindate); - } - if (maxdate) { - maxdate = YAHOO.widget.DateMath.clearTime(maxdate); - } - - html[html.length] = '<tbody class="m' + (useDate.getMonth()+1) + ' ' + this.Style.CSS_BODY + '">'; - - var i = 0; - - var tempDiv = document.createElement("div"); - var cell = document.createElement("td"); - tempDiv.appendChild(cell); - - var cal = this.parent || this; - - for (var r=0;r<6;r++) { - - weekNum = YAHOO.widget.DateMath.getWeekNumber(workingDate, useDate.getFullYear(), startDay); - weekClass = weekPrefix + weekNum; - - // Local OOM check for performance, since we already have pagedate - if (r !== 0 && hideBlankWeeks === true && workingDate.getMonth() != useDate.getMonth()) { - break; - } else { - - html[html.length] = '<tr class="' + weekClass + '">'; - - if (showWeekHeader) { html = this.renderRowHeader(weekNum, html); } - - for (var d=0;d<7;d++){ // Render actual days - - cellRenderers = []; - - this.clearElement(cell); - cell.className = this.Style.CSS_CELL; - cell.id = this.id + cellPrefix + i; - - if (workingDate.getDate() == todayDate && - workingDate.getMonth() == todayMonth && - workingDate.getFullYear() == todayYear) { - cellRenderers[cellRenderers.length]=cal.renderCellStyleToday; - } - - var workingArray = [workingDate.getFullYear(),workingDate.getMonth()+1,workingDate.getDate()]; - this.cellDates[this.cellDates.length] = workingArray; // Add this date to cellDates - - // Local OOM check for performance, since we already have pagedate - if (workingDate.getMonth() != useDate.getMonth()) { - cellRenderers[cellRenderers.length]=cal.renderCellNotThisMonth; - } else { - YAHOO.util.Dom.addClass(cell, workingDayPrefix + workingDate.getDay()); - YAHOO.util.Dom.addClass(cell, dayPrefix + workingDate.getDate()); - - for (var s=0;s<this.renderStack.length;++s) { - - renderer = null; - - var rArray = this.renderStack[s]; - var type = rArray[0]; - - var month; - var day; - var year; - - switch (type) { - case YAHOO.widget.Calendar.DATE: - month = rArray[1][1]; - day = rArray[1][2]; - year = rArray[1][0]; - - if (workingDate.getMonth()+1 == month && workingDate.getDate() == day && workingDate.getFullYear() == year) { - renderer = rArray[2]; - this.renderStack.splice(s,1); - } - break; - case YAHOO.widget.Calendar.MONTH_DAY: - month = rArray[1][0]; - day = rArray[1][1]; - - if (workingDate.getMonth()+1 == month && workingDate.getDate() == day) { - renderer = rArray[2]; - this.renderStack.splice(s,1); - } - break; - case YAHOO.widget.Calendar.RANGE: - var date1 = rArray[1][0]; - var date2 = rArray[1][1]; - - var d1month = date1[1]; - var d1day = date1[2]; - var d1year = date1[0]; - - var d1 = YAHOO.widget.DateMath.getDate(d1year, d1month-1, d1day); - - var d2month = date2[1]; - var d2day = date2[2]; - var d2year = date2[0]; - - var d2 = YAHOO.widget.DateMath.getDate(d2year, d2month-1, d2day); - - if (workingDate.getTime() >= d1.getTime() && workingDate.getTime() <= d2.getTime()) { - renderer = rArray[2]; - - if (workingDate.getTime()==d2.getTime()) { - this.renderStack.splice(s,1); - } - } - break; - case YAHOO.widget.Calendar.WEEKDAY: - - var weekday = rArray[1][0]; - if (workingDate.getDay()+1 == weekday) { - renderer = rArray[2]; - } - break; - case YAHOO.widget.Calendar.MONTH: - - month = rArray[1][0]; - if (workingDate.getMonth()+1 == month) { - renderer = rArray[2]; - } - break; - } - - if (renderer) { - cellRenderers[cellRenderers.length]=renderer; - } - } - - } - - if (this._indexOfSelectedFieldArray(workingArray) > -1) { - cellRenderers[cellRenderers.length]=cal.renderCellStyleSelected; - } - - if ((mindate && (workingDate.getTime() < mindate.getTime())) || - (maxdate && (workingDate.getTime() > maxdate.getTime())) - ) { - cellRenderers[cellRenderers.length]=cal.renderOutOfBoundsDate; - } else { - cellRenderers[cellRenderers.length]=cal.styleCellDefault; - cellRenderers[cellRenderers.length]=cal.renderCellDefault; - } - - for (var x=0; x < cellRenderers.length; ++x) { - if (cellRenderers[x].call(cal, workingDate, cell) == YAHOO.widget.Calendar.STOP_RENDER) { - break; - } - } - - workingDate.setTime(workingDate.getTime() + YAHOO.widget.DateMath.ONE_DAY_MS); - - if (i >= 0 && i <= 6) { - YAHOO.util.Dom.addClass(cell, this.Style.CSS_CELL_TOP); - } - if ((i % 7) === 0) { - YAHOO.util.Dom.addClass(cell, this.Style.CSS_CELL_LEFT); - } - if (((i+1) % 7) === 0) { - YAHOO.util.Dom.addClass(cell, this.Style.CSS_CELL_RIGHT); - } - - var postDays = this.postMonthDays; - if (hideBlankWeeks && postDays >= 7) { - var blankWeeks = Math.floor(postDays/7); - for (var p=0;p<blankWeeks;++p) { - postDays -= 7; - } - } - - if (i >= ((this.preMonthDays+postDays+this.monthDays)-7)) { - YAHOO.util.Dom.addClass(cell, this.Style.CSS_CELL_BOTTOM); - } - - html[html.length] = tempDiv.innerHTML; - i++; - } - - if (showWeekFooter) { html = this.renderRowFooter(weekNum, html); } - - html[html.length] = '</tr>'; - } - } - - html[html.length] = '</tbody>'; - - return html; - }, - - /** - * Renders the calendar footer. In the default implementation, there is - * no footer. - * @method renderFooter - * @param {Array} html The current working HTML array - * @return {Array} The current working HTML array - */ - renderFooter : function(html) { return html; }, - - /** - * Renders the calendar after it has been configured. The render() method has a specific call chain that will execute - * when the method is called: renderHeader, renderBody, renderFooter. - * Refer to the documentation for those methods for information on - * individual render tasks. - * @method render - */ - render : function() { - this.beforeRenderEvent.fire(); - - var defCfg = YAHOO.widget.Calendar._DEFAULT_CONFIG; - - // Find starting day of the current month - var workingDate = YAHOO.widget.DateMath.findMonthStart(this.cfg.getProperty(defCfg.PAGEDATE.key)); - - this.resetRenderers(); - this.cellDates.length = 0; - - YAHOO.util.Event.purgeElement(this.oDomContainer, true); - - var html = []; - - html[html.length] = '<table cellSpacing="0" class="' + this.Style.CSS_CALENDAR + ' y' + workingDate.getFullYear() + '" id="' + this.id + '">'; - html = this.renderHeader(html); - html = this.renderBody(workingDate, html); - html = this.renderFooter(html); - html[html.length] = '</table>'; - - this.oDomContainer.innerHTML = html.join("\n"); - - this.applyListeners(); - this.cells = this.oDomContainer.getElementsByTagName("td"); - - this.cfg.refireEvent(defCfg.TITLE.key); - this.cfg.refireEvent(defCfg.CLOSE.key); - this.cfg.refireEvent(defCfg.IFRAME.key); - - this.renderEvent.fire(); - }, - - /** - * Applies the Calendar's DOM listeners to applicable elements. - * @method applyListeners - */ - applyListeners : function() { - var root = this.oDomContainer; - var cal = this.parent || this; - var anchor = "a"; - var mousedown = "mousedown"; - - var linkLeft = YAHOO.util.Dom.getElementsByClassName(this.Style.CSS_NAV_LEFT, anchor, root); - var linkRight = YAHOO.util.Dom.getElementsByClassName(this.Style.CSS_NAV_RIGHT, anchor, root); - - if (linkLeft && linkLeft.length > 0) { - this.linkLeft = linkLeft[0]; - YAHOO.util.Event.addListener(this.linkLeft, mousedown, cal.previousMonth, cal, true); - } - - if (linkRight && linkRight.length > 0) { - this.linkRight = linkRight[0]; - YAHOO.util.Event.addListener(this.linkRight, mousedown, cal.nextMonth, cal, true); - } - - if (cal.cfg.getProperty("navigator") !== null) { - this.applyNavListeners(); - } - - if (this.domEventMap) { - var el,elements; - for (var cls in this.domEventMap) { - if (YAHOO.lang.hasOwnProperty(this.domEventMap, cls)) { - var items = this.domEventMap[cls]; - - if (! (items instanceof Array)) { - items = [items]; - } - - for (var i=0;i<items.length;i++) { - var item = items[i]; - elements = YAHOO.util.Dom.getElementsByClassName(cls, item.tag, this.oDomContainer); - - for (var c=0;c<elements.length;c++) { - el = elements[c]; - YAHOO.util.Event.addListener(el, item.event, item.handler, item.scope, item.correct ); - } - } - } - } - } - - YAHOO.util.Event.addListener(this.oDomContainer, "click", this.doSelectCell, this); - YAHOO.util.Event.addListener(this.oDomContainer, "mouseover", this.doCellMouseOver, this); - YAHOO.util.Event.addListener(this.oDomContainer, "mouseout", this.doCellMouseOut, this); - }, - - applyNavListeners : function() { - - var E = YAHOO.util.Event; - - var calParent = this.parent || this; - var cal = this; - - var navBtns = YAHOO.util.Dom.getElementsByClassName(this.Style.CSS_NAV, "a", this.oDomContainer); - - if (navBtns.length > 0) { - - function show(e, obj) { - var target = E.getTarget(e); - // this == navBtn - if (this === target || YAHOO.util.Dom.isAncestor(this, target)) { - E.preventDefault(e); - } - var navigator = calParent.oNavigator; - if (navigator) { - var pgdate = cal.cfg.getProperty("pagedate"); - navigator.setYear(pgdate.getFullYear()); - navigator.setMonth(pgdate.getMonth()); - navigator.show(); - } - } - E.addListener(navBtns, "click", show); - } - }, - - /** - * Retrieves the Date object for the specified Calendar cell - * @method getDateByCellId - * @param {String} id The id of the cell - * @return {Date} The Date object for the specified Calendar cell - */ - getDateByCellId : function(id) { - var date = this.getDateFieldsByCellId(id); - return YAHOO.widget.DateMath.getDate(date[0],date[1]-1,date[2]); - }, - - /** - * Retrieves the Date object for the specified Calendar cell - * @method getDateFieldsByCellId - * @param {String} id The id of the cell - * @return {Array} The array of Date fields for the specified Calendar cell - */ - getDateFieldsByCellId : function(id) { - id = id.toLowerCase().split("_cell")[1]; - id = parseInt(id, 10); - return this.cellDates[id]; - }, - - /** - * Find the Calendar's cell index for a given date. - * If the date is not found, the method returns -1. - * <p> - * The returned index can be used to lookup the cell HTMLElement - * using the Calendar's cells array or passed to selectCell to select - * cells by index. - * </p> - * - * See <a href="#cells">cells</a>, <a href="#selectCell">selectCell</a>. - * - * @method getCellIndex - * @param {Date} date JavaScript Date object, for which to find a cell index. - * @return {Number} The index of the date in Calendars cellDates/cells arrays, or -1 if the date - * is not on the curently rendered Calendar page. - */ - getCellIndex : function(date) { - var idx = -1; - if (date) { - var m = date.getMonth(), - y = date.getFullYear(), - d = date.getDate(), - dates = this.cellDates; - - for (var i = 0; i < dates.length; ++i) { - var cellDate = dates[i]; - if (cellDate[0] === y && cellDate[1] === m+1 && cellDate[2] === d) { - idx = i; - break; - } - } - } - return idx; - }, - - // BEGIN BUILT-IN TABLE CELL RENDERERS - - /** - * Renders a cell that falls before the minimum date or after the maximum date. - * widget class. - * @method renderOutOfBoundsDate - * @param {Date} workingDate The current working Date object being used to generate the calendar - * @param {HTMLTableCellElement} cell The current working cell in the calendar - * @return {String} YAHOO.widget.Calendar.STOP_RENDER if rendering should stop with this style, null or nothing if rendering - * should not be terminated - */ - renderOutOfBoundsDate : function(workingDate, cell) { - YAHOO.util.Dom.addClass(cell, this.Style.CSS_CELL_OOB); - cell.innerHTML = workingDate.getDate(); - return YAHOO.widget.Calendar.STOP_RENDER; - }, - - /** - * Renders the row header for a week. - * @method renderRowHeader - * @param {Number} weekNum The week number of the current row - * @param {Array} cell The current working HTML array - */ - renderRowHeader : function(weekNum, html) { - html[html.length] = '<th class="calrowhead">' + weekNum + '</th>'; - return html; - }, - - /** - * Renders the row footer for a week. - * @method renderRowFooter - * @param {Number} weekNum The week number of the current row - * @param {Array} cell The current working HTML array - */ - renderRowFooter : function(weekNum, html) { - html[html.length] = '<th class="calrowfoot">' + weekNum + '</th>'; - return html; - }, - - /** - * Renders a single standard calendar cell in the calendar widget table. - * All logic for determining how a standard default cell will be rendered is - * encapsulated in this method, and must be accounted for when extending the - * widget class. - * @method renderCellDefault - * @param {Date} workingDate The current working Date object being used to generate the calendar - * @param {HTMLTableCellElement} cell The current working cell in the calendar - */ - renderCellDefault : function(workingDate, cell) { - cell.innerHTML = '<a href="#" class="' + this.Style.CSS_CELL_SELECTOR + '">' + this.buildDayLabel(workingDate) + "</a>"; - }, - - /** - * Styles a selectable cell. - * @method styleCellDefault - * @param {Date} workingDate The current working Date object being used to generate the calendar - * @param {HTMLTableCellElement} cell The current working cell in the calendar - */ - styleCellDefault : function(workingDate, cell) { - YAHOO.util.Dom.addClass(cell, this.Style.CSS_CELL_SELECTABLE); - }, - - - /** - * Renders a single standard calendar cell using the CSS hightlight1 style - * @method renderCellStyleHighlight1 - * @param {Date} workingDate The current working Date object being used to generate the calendar - * @param {HTMLTableCellElement} cell The current working cell in the calendar - */ - renderCellStyleHighlight1 : function(workingDate, cell) { - YAHOO.util.Dom.addClass(cell, this.Style.CSS_CELL_HIGHLIGHT1); - }, - - /** - * Renders a single standard calendar cell using the CSS hightlight2 style - * @method renderCellStyleHighlight2 - * @param {Date} workingDate The current working Date object being used to generate the calendar - * @param {HTMLTableCellElement} cell The current working cell in the calendar - */ - renderCellStyleHighlight2 : function(workingDate, cell) { - YAHOO.util.Dom.addClass(cell, this.Style.CSS_CELL_HIGHLIGHT2); - }, - - /** - * Renders a single standard calendar cell using the CSS hightlight3 style - * @method renderCellStyleHighlight3 - * @param {Date} workingDate The current working Date object being used to generate the calendar - * @param {HTMLTableCellElement} cell The current working cell in the calendar - */ - renderCellStyleHighlight3 : function(workingDate, cell) { - YAHOO.util.Dom.addClass(cell, this.Style.CSS_CELL_HIGHLIGHT3); - }, - - /** - * Renders a single standard calendar cell using the CSS hightlight4 style - * @method renderCellStyleHighlight4 - * @param {Date} workingDate The current working Date object being used to generate the calendar - * @param {HTMLTableCellElement} cell The current working cell in the calendar - */ - renderCellStyleHighlight4 : function(workingDate, cell) { - YAHOO.util.Dom.addClass(cell, this.Style.CSS_CELL_HIGHLIGHT4); - }, - - /** - * Applies the default style used for rendering today's date to the current calendar cell - * @method renderCellStyleToday - * @param {Date} workingDate The current working Date object being used to generate the calendar - * @param {HTMLTableCellElement} cell The current working cell in the calendar - */ - renderCellStyleToday : function(workingDate, cell) { - YAHOO.util.Dom.addClass(cell, this.Style.CSS_CELL_TODAY); - }, - - /** - * Applies the default style used for rendering selected dates to the current calendar cell - * @method renderCellStyleSelected - * @param {Date} workingDate The current working Date object being used to generate the calendar - * @param {HTMLTableCellElement} cell The current working cell in the calendar - * @return {String} YAHOO.widget.Calendar.STOP_RENDER if rendering should stop with this style, null or nothing if rendering - * should not be terminated - */ - renderCellStyleSelected : function(workingDate, cell) { - YAHOO.util.Dom.addClass(cell, this.Style.CSS_CELL_SELECTED); - }, - - /** - * Applies the default style used for rendering dates that are not a part of the current - * month (preceding or trailing the cells for the current month) - * @method renderCellNotThisMonth - * @param {Date} workingDate The current working Date object being used to generate the calendar - * @param {HTMLTableCellElement} cell The current working cell in the calendar - * @return {String} YAHOO.widget.Calendar.STOP_RENDER if rendering should stop with this style, null or nothing if rendering - * should not be terminated - */ - renderCellNotThisMonth : function(workingDate, cell) { - YAHOO.util.Dom.addClass(cell, this.Style.CSS_CELL_OOM); - cell.innerHTML=workingDate.getDate(); - return YAHOO.widget.Calendar.STOP_RENDER; - }, - - /** - * Renders the current calendar cell as a non-selectable "black-out" date using the default - * restricted style. - * @method renderBodyCellRestricted - * @param {Date} workingDate The current working Date object being used to generate the calendar - * @param {HTMLTableCellElement} cell The current working cell in the calendar - * @return {String} YAHOO.widget.Calendar.STOP_RENDER if rendering should stop with this style, null or nothing if rendering - * should not be terminated - */ - renderBodyCellRestricted : function(workingDate, cell) { - YAHOO.util.Dom.addClass(cell, this.Style.CSS_CELL); - YAHOO.util.Dom.addClass(cell, this.Style.CSS_CELL_RESTRICTED); - cell.innerHTML=workingDate.getDate(); - return YAHOO.widget.Calendar.STOP_RENDER; - }, - - // END BUILT-IN TABLE CELL RENDERERS - - // BEGIN MONTH NAVIGATION METHODS - - /** - * Adds the designated number of months to the current calendar month, and sets the current - * calendar page date to the new month. - * @method addMonths - * @param {Number} count The number of months to add to the current calendar - */ - addMonths : function(count) { - var cfgPageDate = YAHOO.widget.Calendar._DEFAULT_CONFIG.PAGEDATE.key; - this.cfg.setProperty(cfgPageDate, YAHOO.widget.DateMath.add(this.cfg.getProperty(cfgPageDate), YAHOO.widget.DateMath.MONTH, count)); - this.resetRenderers(); - this.changePageEvent.fire(); - }, - - /** - * Subtracts the designated number of months from the current calendar month, and sets the current - * calendar page date to the new month. - * @method subtractMonths - * @param {Number} count The number of months to subtract from the current calendar - */ - subtractMonths : function(count) { - var cfgPageDate = YAHOO.widget.Calendar._DEFAULT_CONFIG.PAGEDATE.key; - this.cfg.setProperty(cfgPageDate, YAHOO.widget.DateMath.subtract(this.cfg.getProperty(cfgPageDate), YAHOO.widget.DateMath.MONTH, count)); - this.resetRenderers(); - this.changePageEvent.fire(); - }, - - /** - * Adds the designated number of years to the current calendar, and sets the current - * calendar page date to the new month. - * @method addYears - * @param {Number} count The number of years to add to the current calendar - */ - addYears : function(count) { - var cfgPageDate = YAHOO.widget.Calendar._DEFAULT_CONFIG.PAGEDATE.key; - this.cfg.setProperty(cfgPageDate, YAHOO.widget.DateMath.add(this.cfg.getProperty(cfgPageDate), YAHOO.widget.DateMath.YEAR, count)); - this.resetRenderers(); - this.changePageEvent.fire(); - }, - - /** - * Subtcats the designated number of years from the current calendar, and sets the current - * calendar page date to the new month. - * @method subtractYears - * @param {Number} count The number of years to subtract from the current calendar - */ - subtractYears : function(count) { - var cfgPageDate = YAHOO.widget.Calendar._DEFAULT_CONFIG.PAGEDATE.key; - this.cfg.setProperty(cfgPageDate, YAHOO.widget.DateMath.subtract(this.cfg.getProperty(cfgPageDate), YAHOO.widget.DateMath.YEAR, count)); - this.resetRenderers(); - this.changePageEvent.fire(); - }, - - /** - * Navigates to the next month page in the calendar widget. - * @method nextMonth - */ - nextMonth : function() { - this.addMonths(1); - }, - - /** - * Navigates to the previous month page in the calendar widget. - * @method previousMonth - */ - previousMonth : function() { - this.subtractMonths(1); - }, - - /** - * Navigates to the next year in the currently selected month in the calendar widget. - * @method nextYear - */ - nextYear : function() { - this.addYears(1); - }, - - /** - * Navigates to the previous year in the currently selected month in the calendar widget. - * @method previousYear - */ - previousYear : function() { - this.subtractYears(1); - }, - - // END MONTH NAVIGATION METHODS - - // BEGIN SELECTION METHODS - - /** - * Resets the calendar widget to the originally selected month and year, and - * sets the calendar to the initial selection(s). - * @method reset - */ - reset : function() { - var defCfg = YAHOO.widget.Calendar._DEFAULT_CONFIG; - this.cfg.resetProperty(defCfg.SELECTED.key); - this.cfg.resetProperty(defCfg.PAGEDATE.key); - this.resetEvent.fire(); - }, - - /** - * Clears the selected dates in the current calendar widget and sets the calendar - * to the current month and year. - * @method clear - */ - clear : function() { - var defCfg = YAHOO.widget.Calendar._DEFAULT_CONFIG; - this.cfg.setProperty(defCfg.SELECTED.key, []); - this.cfg.setProperty(defCfg.PAGEDATE.key, new Date(this.today.getTime())); - this.clearEvent.fire(); - }, - - /** - * Selects a date or a collection of dates on the current calendar. This method, by default, - * does not call the render method explicitly. Once selection has completed, render must be - * called for the changes to be reflected visually. - * - * Any dates which are OOB (out of bounds, not selectable) will not be selected and the array of - * selected dates passed to the selectEvent will not contain OOB dates. - * - * If all dates are OOB, the no state change will occur; beforeSelect and select events will not be fired. - * - * @method select - * @param {String/Date/Date[]} date The date string of dates to select in the current calendar. Valid formats are - * individual date(s) (12/24/2005,12/26/2005) or date range(s) (12/24/2005-1/1/2006). - * Multiple comma-delimited dates can also be passed to this method (12/24/2005,12/11/2005-12/13/2005). - * This method can also take a JavaScript Date object or an array of Date objects. - * @return {Date[]} Array of JavaScript Date objects representing all individual dates that are currently selected. - */ - select : function(date) { - - var aToBeSelected = this._toFieldArray(date); - - // Filtered array of valid dates - var validDates = []; - var selected = []; - var cfgSelected = YAHOO.widget.Calendar._DEFAULT_CONFIG.SELECTED.key; - - for (var a=0; a < aToBeSelected.length; ++a) { - var toSelect = aToBeSelected[a]; - - if (!this.isDateOOB(this._toDate(toSelect))) { - - if (validDates.length === 0) { - this.beforeSelectEvent.fire(); - selected = this.cfg.getProperty(cfgSelected); - } - - validDates.push(toSelect); - - if (this._indexOfSelectedFieldArray(toSelect) == -1) { - selected[selected.length] = toSelect; - } - } - } - - - if (validDates.length > 0) { - if (this.parent) { - this.parent.cfg.setProperty(cfgSelected, selected); - } else { - this.cfg.setProperty(cfgSelected, selected); - } - this.selectEvent.fire(validDates); - } - - return this.getSelectedDates(); - }, - - /** - * Selects a date on the current calendar by referencing the index of the cell that should be selected. - * This method is used to easily select a single cell (usually with a mouse click) without having to do - * a full render. The selected style is applied to the cell directly. - * - * If the cell is not marked with the CSS_CELL_SELECTABLE class (as is the case by default for out of month - * or out of bounds cells), it will not be selected and in such a case beforeSelect and select events will not be fired. - * - * @method selectCell - * @param {Number} cellIndex The index of the cell to select in the current calendar. - * @return {Date[]} Array of JavaScript Date objects representing all individual dates that are currently selected. - */ - selectCell : function(cellIndex) { - - var cell = this.cells[cellIndex]; - var cellDate = this.cellDates[cellIndex]; - var dCellDate = this._toDate(cellDate); - - var selectable = YAHOO.util.Dom.hasClass(cell, this.Style.CSS_CELL_SELECTABLE); - - if (selectable) { - - this.beforeSelectEvent.fire(); - - var cfgSelected = YAHOO.widget.Calendar._DEFAULT_CONFIG.SELECTED.key; - var selected = this.cfg.getProperty(cfgSelected); - - var selectDate = cellDate.concat(); - - if (this._indexOfSelectedFieldArray(selectDate) == -1) { - selected[selected.length] = selectDate; - } - if (this.parent) { - this.parent.cfg.setProperty(cfgSelected, selected); - } else { - this.cfg.setProperty(cfgSelected, selected); - } - this.renderCellStyleSelected(dCellDate,cell); - this.selectEvent.fire([selectDate]); - - this.doCellMouseOut.call(cell, null, this); - } - - return this.getSelectedDates(); - }, - - /** - * Deselects a date or a collection of dates on the current calendar. This method, by default, - * does not call the render method explicitly. Once deselection has completed, render must be - * called for the changes to be reflected visually. - * - * The method will not attempt to deselect any dates which are OOB (out of bounds, and hence not selectable) - * and the array of deselected dates passed to the deselectEvent will not contain any OOB dates. - * - * If all dates are OOB, beforeDeselect and deselect events will not be fired. - * - * @method deselect - * @param {String/Date/Date[]} date The date string of dates to deselect in the current calendar. Valid formats are - * individual date(s) (12/24/2005,12/26/2005) or date range(s) (12/24/2005-1/1/2006). - * Multiple comma-delimited dates can also be passed to this method (12/24/2005,12/11/2005-12/13/2005). - * This method can also take a JavaScript Date object or an array of Date objects. - * @return {Date[]} Array of JavaScript Date objects representing all individual dates that are currently selected. - */ - deselect : function(date) { - - var aToBeDeselected = this._toFieldArray(date); - - var validDates = []; - var selected = []; - var cfgSelected = YAHOO.widget.Calendar._DEFAULT_CONFIG.SELECTED.key; - - for (var a=0; a < aToBeDeselected.length; ++a) { - var toDeselect = aToBeDeselected[a]; - - if (!this.isDateOOB(this._toDate(toDeselect))) { - - if (validDates.length === 0) { - this.beforeDeselectEvent.fire(); - selected = this.cfg.getProperty(cfgSelected); - } - - validDates.push(toDeselect); - - var index = this._indexOfSelectedFieldArray(toDeselect); - if (index != -1) { - selected.splice(index,1); - } - } - } - - - if (validDates.length > 0) { - if (this.parent) { - this.parent.cfg.setProperty(cfgSelected, selected); - } else { - this.cfg.setProperty(cfgSelected, selected); - } - this.deselectEvent.fire(validDates); - } - - return this.getSelectedDates(); - }, - - /** - * Deselects a date on the current calendar by referencing the index of the cell that should be deselected. - * This method is used to easily deselect a single cell (usually with a mouse click) without having to do - * a full render. The selected style is removed from the cell directly. - * - * If the cell is not marked with the CSS_CELL_SELECTABLE class (as is the case by default for out of month - * or out of bounds cells), the method will not attempt to deselect it and in such a case, beforeDeselect and - * deselect events will not be fired. - * - * @method deselectCell - * @param {Number} cellIndex The index of the cell to deselect in the current calendar. - * @return {Date[]} Array of JavaScript Date objects representing all individual dates that are currently selected. - */ - deselectCell : function(cellIndex) { - var cell = this.cells[cellIndex]; - var cellDate = this.cellDates[cellIndex]; - var cellDateIndex = this._indexOfSelectedFieldArray(cellDate); - - var selectable = YAHOO.util.Dom.hasClass(cell, this.Style.CSS_CELL_SELECTABLE); - - if (selectable) { - - this.beforeDeselectEvent.fire(); - - var defCfg = YAHOO.widget.Calendar._DEFAULT_CONFIG; - var selected = this.cfg.getProperty(defCfg.SELECTED.key); - - var dCellDate = this._toDate(cellDate); - var selectDate = cellDate.concat(); - - if (cellDateIndex > -1) { - if (this.cfg.getProperty(defCfg.PAGEDATE.key).getMonth() == dCellDate.getMonth() && - this.cfg.getProperty(defCfg.PAGEDATE.key).getFullYear() == dCellDate.getFullYear()) { - YAHOO.util.Dom.removeClass(cell, this.Style.CSS_CELL_SELECTED); - } - selected.splice(cellDateIndex, 1); - } - - if (this.parent) { - this.parent.cfg.setProperty(defCfg.SELECTED.key, selected); - } else { - this.cfg.setProperty(defCfg.SELECTED.key, selected); - } - - this.deselectEvent.fire(selectDate); - } - - return this.getSelectedDates(); - }, - - /** - * Deselects all dates on the current calendar. - * @method deselectAll - * @return {Date[]} Array of JavaScript Date objects representing all individual dates that are currently selected. - * Assuming that this function executes properly, the return value should be an empty array. - * However, the empty array is returned for the sake of being able to check the selection status - * of the calendar. - */ - deselectAll : function() { - this.beforeDeselectEvent.fire(); - - var cfgSelected = YAHOO.widget.Calendar._DEFAULT_CONFIG.SELECTED.key; - - var selected = this.cfg.getProperty(cfgSelected); - var count = selected.length; - var sel = selected.concat(); - - if (this.parent) { - this.parent.cfg.setProperty(cfgSelected, []); - } else { - this.cfg.setProperty(cfgSelected, []); - } - - if (count > 0) { - this.deselectEvent.fire(sel); - } - - return this.getSelectedDates(); - }, - - // END SELECTION METHODS - - // BEGIN TYPE CONVERSION METHODS - - /** - * Converts a date (either a JavaScript Date object, or a date string) to the internal data structure - * used to represent dates: [[yyyy,mm,dd],[yyyy,mm,dd]]. - * @method _toFieldArray - * @private - * @param {String/Date/Date[]} date The date string of dates to deselect in the current calendar. Valid formats are - * individual date(s) (12/24/2005,12/26/2005) or date range(s) (12/24/2005-1/1/2006). - * Multiple comma-delimited dates can also be passed to this method (12/24/2005,12/11/2005-12/13/2005). - * This method can also take a JavaScript Date object or an array of Date objects. - * @return {Array[](Number[])} Array of date field arrays - */ - _toFieldArray : function(date) { - var returnDate = []; - - if (date instanceof Date) { - returnDate = [[date.getFullYear(), date.getMonth()+1, date.getDate()]]; - } else if (YAHOO.lang.isString(date)) { - returnDate = this._parseDates(date); - } else if (YAHOO.lang.isArray(date)) { - for (var i=0;i<date.length;++i) { - var d = date[i]; - returnDate[returnDate.length] = [d.getFullYear(),d.getMonth()+1,d.getDate()]; - } - } - - return returnDate; - }, - - /** - * Converts a date field array [yyyy,mm,dd] to a JavaScript Date object. The date field array - * is the format in which dates are as provided as arguments to selectEvent and deselectEvent listeners. - * - * @method toDate - * @param {Number[]} dateFieldArray The date field array to convert to a JavaScript Date. - * @return {Date} JavaScript Date object representing the date field array. - */ - toDate : function(dateFieldArray) { - return this._toDate(dateFieldArray); - }, - - /** - * Converts a date field array [yyyy,mm,dd] to a JavaScript Date object. - * @method _toDate - * @private - * @deprecated Made public, toDate - * @param {Number[]} dateFieldArray The date field array to convert to a JavaScript Date. - * @return {Date} JavaScript Date object representing the date field array - */ - _toDate : function(dateFieldArray) { - if (dateFieldArray instanceof Date) { - return dateFieldArray; - } else { - return YAHOO.widget.DateMath.getDate(dateFieldArray[0],dateFieldArray[1]-1,dateFieldArray[2]); - } - }, - - // END TYPE CONVERSION METHODS - - // BEGIN UTILITY METHODS - - /** - * Converts a date field array [yyyy,mm,dd] to a JavaScript Date object. - * @method _fieldArraysAreEqual - * @private - * @param {Number[]} array1 The first date field array to compare - * @param {Number[]} array2 The first date field array to compare - * @return {Boolean} The boolean that represents the equality of the two arrays - */ - _fieldArraysAreEqual : function(array1, array2) { - var match = false; - - if (array1[0]==array2[0]&&array1[1]==array2[1]&&array1[2]==array2[2]) { - match=true; - } - - return match; - }, - - /** - * Gets the index of a date field array [yyyy,mm,dd] in the current list of selected dates. - * @method _indexOfSelectedFieldArray - * @private - * @param {Number[]} find The date field array to search for - * @return {Number} The index of the date field array within the collection of selected dates. - * -1 will be returned if the date is not found. - */ - _indexOfSelectedFieldArray : function(find) { - var selected = -1; - var seldates = this.cfg.getProperty(YAHOO.widget.Calendar._DEFAULT_CONFIG.SELECTED.key); - - for (var s=0;s<seldates.length;++s) { - var sArray = seldates[s]; - if (find[0]==sArray[0]&&find[1]==sArray[1]&&find[2]==sArray[2]) { - selected = s; - break; - } - } - - return selected; - }, - - /** - * Determines whether a given date is OOM (out of month). - * @method isDateOOM - * @param {Date} date The JavaScript Date object for which to check the OOM status - * @return {Boolean} true if the date is OOM - */ - isDateOOM : function(date) { - return (date.getMonth() != this.cfg.getProperty(YAHOO.widget.Calendar._DEFAULT_CONFIG.PAGEDATE.key).getMonth()); - }, - - /** - * Determines whether a given date is OOB (out of bounds - less than the mindate or more than the maxdate). - * - * @method isDateOOB - * @param {Date} date The JavaScript Date object for which to check the OOB status - * @return {Boolean} true if the date is OOB - */ - isDateOOB : function(date) { - var defCfg = YAHOO.widget.Calendar._DEFAULT_CONFIG; - - var minDate = this.cfg.getProperty(defCfg.MINDATE.key); - var maxDate = this.cfg.getProperty(defCfg.MAXDATE.key); - var dm = YAHOO.widget.DateMath; - - if (minDate) { - minDate = dm.clearTime(minDate); - } - if (maxDate) { - maxDate = dm.clearTime(maxDate); - } - - var clearedDate = new Date(date.getTime()); - clearedDate = dm.clearTime(clearedDate); - - return ((minDate && clearedDate.getTime() < minDate.getTime()) || (maxDate && clearedDate.getTime() > maxDate.getTime())); - }, - - /** - * Parses a pagedate configuration property value. The value can either be specified as a string of form "mm/yyyy" or a Date object - * and is parsed into a Date object normalized to the first day of the month. If no value is passed in, the month and year from today's date are used to create the Date object - * @method _parsePageDate - * @private - * @param {Date|String} date Pagedate value which needs to be parsed - * @return {Date} The Date object representing the pagedate - */ - _parsePageDate : function(date) { - var parsedDate; - - var defCfg = YAHOO.widget.Calendar._DEFAULT_CONFIG; - - if (date) { - if (date instanceof Date) { - parsedDate = YAHOO.widget.DateMath.findMonthStart(date); - } else { - var month, year, aMonthYear; - aMonthYear = date.split(this.cfg.getProperty(defCfg.DATE_FIELD_DELIMITER.key)); - month = parseInt(aMonthYear[this.cfg.getProperty(defCfg.MY_MONTH_POSITION.key)-1], 10)-1; - year = parseInt(aMonthYear[this.cfg.getProperty(defCfg.MY_YEAR_POSITION.key)-1], 10); - - parsedDate = YAHOO.widget.DateMath.getDate(year, month, 1); - } - } else { - parsedDate = YAHOO.widget.DateMath.getDate(this.today.getFullYear(), this.today.getMonth(), 1); - } - return parsedDate; - }, - - // END UTILITY METHODS - - // BEGIN EVENT HANDLERS - - /** - * Event executed before a date is selected in the calendar widget. - * @deprecated Event handlers for this event should be susbcribed to beforeSelectEvent. - */ - onBeforeSelect : function() { - if (this.cfg.getProperty(YAHOO.widget.Calendar._DEFAULT_CONFIG.MULTI_SELECT.key) === false) { - if (this.parent) { - this.parent.callChildFunction("clearAllBodyCellStyles", this.Style.CSS_CELL_SELECTED); - this.parent.deselectAll(); - } else { - this.clearAllBodyCellStyles(this.Style.CSS_CELL_SELECTED); - this.deselectAll(); - } - } - }, - - /** - * Event executed when a date is selected in the calendar widget. - * @param {Array} selected An array of date field arrays representing which date or dates were selected. Example: [ [2006,8,6],[2006,8,7],[2006,8,8] ] - * @deprecated Event handlers for this event should be susbcribed to selectEvent. - */ - onSelect : function(selected) { }, - - /** - * Event executed before a date is deselected in the calendar widget. - * @deprecated Event handlers for this event should be susbcribed to beforeDeselectEvent. - */ - onBeforeDeselect : function() { }, - - /** - * Event executed when a date is deselected in the calendar widget. - * @param {Array} selected An array of date field arrays representing which date or dates were deselected. Example: [ [2006,8,6],[2006,8,7],[2006,8,8] ] - * @deprecated Event handlers for this event should be susbcribed to deselectEvent. - */ - onDeselect : function(deselected) { }, - - /** - * Event executed when the user navigates to a different calendar page. - * @deprecated Event handlers for this event should be susbcribed to changePageEvent. - */ - onChangePage : function() { - this.render(); - }, - - /** - * Event executed when the calendar widget is rendered. - * @deprecated Event handlers for this event should be susbcribed to renderEvent. - */ - onRender : function() { }, - - /** - * Event executed when the calendar widget is reset to its original state. - * @deprecated Event handlers for this event should be susbcribed to resetEvemt. - */ - onReset : function() { this.render(); }, - - /** - * Event executed when the calendar widget is completely cleared to the current month with no selections. - * @deprecated Event handlers for this event should be susbcribed to clearEvent. - */ - onClear : function() { this.render(); }, - - /** - * Validates the calendar widget. This method has no default implementation - * and must be extended by subclassing the widget. - * @return Should return true if the widget validates, and false if - * it doesn't. - * @type Boolean - */ - validate : function() { return true; }, - - // END EVENT HANDLERS - - // BEGIN DATE PARSE METHODS - - /** - * Converts a date string to a date field array - * @private - * @param {String} sDate Date string. Valid formats are mm/dd and mm/dd/yyyy. - * @return A date field array representing the string passed to the method - * @type Array[](Number[]) - */ - _parseDate : function(sDate) { - var aDate = sDate.split(this.Locale.DATE_FIELD_DELIMITER); - var rArray; - - if (aDate.length == 2) { - rArray = [aDate[this.Locale.MD_MONTH_POSITION-1],aDate[this.Locale.MD_DAY_POSITION-1]]; - rArray.type = YAHOO.widget.Calendar.MONTH_DAY; - } else { - rArray = [aDate[this.Locale.MDY_YEAR_POSITION-1],aDate[this.Locale.MDY_MONTH_POSITION-1],aDate[this.Locale.MDY_DAY_POSITION-1]]; - rArray.type = YAHOO.widget.Calendar.DATE; - } - - for (var i=0;i<rArray.length;i++) { - rArray[i] = parseInt(rArray[i], 10); - } - - return rArray; - }, - - /** - * Converts a multi or single-date string to an array of date field arrays - * @private - * @param {String} sDates Date string with one or more comma-delimited dates. Valid formats are mm/dd, mm/dd/yyyy, mm/dd/yyyy-mm/dd/yyyy - * @return An array of date field arrays - * @type Array[](Number[]) - */ - _parseDates : function(sDates) { - var aReturn = []; - - var aDates = sDates.split(this.Locale.DATE_DELIMITER); - - for (var d=0;d<aDates.length;++d) { - var sDate = aDates[d]; - - if (sDate.indexOf(this.Locale.DATE_RANGE_DELIMITER) != -1) { - // This is a range - var aRange = sDate.split(this.Locale.DATE_RANGE_DELIMITER); - - var dateStart = this._parseDate(aRange[0]); - var dateEnd = this._parseDate(aRange[1]); - - var fullRange = this._parseRange(dateStart, dateEnd); - aReturn = aReturn.concat(fullRange); - } else { - // This is not a range - var aDate = this._parseDate(sDate); - aReturn.push(aDate); - } - } - return aReturn; - }, - - /** - * Converts a date range to the full list of included dates - * @private - * @param {Number[]} startDate Date field array representing the first date in the range - * @param {Number[]} endDate Date field array representing the last date in the range - * @return An array of date field arrays - * @type Array[](Number[]) - */ - _parseRange : function(startDate, endDate) { - var dCurrent = YAHOO.widget.DateMath.add(YAHOO.widget.DateMath.getDate(startDate[0],startDate[1]-1,startDate[2]),YAHOO.widget.DateMath.DAY,1); - var dEnd = YAHOO.widget.DateMath.getDate(endDate[0], endDate[1]-1, endDate[2]); - - var results = []; - results.push(startDate); - while (dCurrent.getTime() <= dEnd.getTime()) { - results.push([dCurrent.getFullYear(),dCurrent.getMonth()+1,dCurrent.getDate()]); - dCurrent = YAHOO.widget.DateMath.add(dCurrent,YAHOO.widget.DateMath.DAY,1); - } - return results; - }, - - // END DATE PARSE METHODS - - // BEGIN RENDERER METHODS - - /** - * Resets the render stack of the current calendar to its original pre-render value. - */ - resetRenderers : function() { - this.renderStack = this._renderStack.concat(); - }, - - /** - * Removes all custom renderers added to the Calendar through the addRenderer, addMonthRenderer and - * addWeekdayRenderer methods. Calendar's render method needs to be called after removing renderers - * to re-render the Calendar without custom renderers applied. - */ - removeRenderers : function() { - this._renderStack = []; - this.renderStack = []; - }, - - /** - * Clears the inner HTML, CSS class and style information from the specified cell. - * @method clearElement - * @param {HTMLTableCellElement} cell The cell to clear - */ - clearElement : function(cell) { - cell.innerHTML = " "; - cell.className=""; - }, - - /** - * Adds a renderer to the render stack. The function reference passed to this method will be executed - * when a date cell matches the conditions specified in the date string for this renderer. - * @method addRenderer - * @param {String} sDates A date string to associate with the specified renderer. Valid formats - * include date (12/24/2005), month/day (12/24), and range (12/1/2004-1/1/2005) - * @param {Function} fnRender The function executed to render cells that match the render rules for this renderer. - */ - addRenderer : function(sDates, fnRender) { - var aDates = this._parseDates(sDates); - for (var i=0;i<aDates.length;++i) { - var aDate = aDates[i]; - - if (aDate.length == 2) { // this is either a range or a month/day combo - if (aDate[0] instanceof Array) { // this is a range - this._addRenderer(YAHOO.widget.Calendar.RANGE,aDate,fnRender); - } else { // this is a month/day combo - this._addRenderer(YAHOO.widget.Calendar.MONTH_DAY,aDate,fnRender); - } - } else if (aDate.length == 3) { - this._addRenderer(YAHOO.widget.Calendar.DATE,aDate,fnRender); - } - } - }, - - /** - * The private method used for adding cell renderers to the local render stack. - * This method is called by other methods that set the renderer type prior to the method call. - * @method _addRenderer - * @private - * @param {String} type The type string that indicates the type of date renderer being added. - * Values are YAHOO.widget.Calendar.DATE, YAHOO.widget.Calendar.MONTH_DAY, YAHOO.widget.Calendar.WEEKDAY, - * YAHOO.widget.Calendar.RANGE, YAHOO.widget.Calendar.MONTH - * @param {Array} aDates An array of dates used to construct the renderer. The format varies based - * on the renderer type - * @param {Function} fnRender The function executed to render cells that match the render rules for this renderer. - */ - _addRenderer : function(type, aDates, fnRender) { - var add = [type,aDates,fnRender]; - this.renderStack.unshift(add); - this._renderStack = this.renderStack.concat(); - }, - - /** - * Adds a month to the render stack. The function reference passed to this method will be executed - * when a date cell matches the month passed to this method. - * @method addMonthRenderer - * @param {Number} month The month (1-12) to associate with this renderer - * @param {Function} fnRender The function executed to render cells that match the render rules for this renderer. - */ - addMonthRenderer : function(month, fnRender) { - this._addRenderer(YAHOO.widget.Calendar.MONTH,[month],fnRender); - }, - - /** - * Adds a weekday to the render stack. The function reference passed to this method will be executed - * when a date cell matches the weekday passed to this method. - * @method addWeekdayRenderer - * @param {Number} weekday The weekday (0-6) to associate with this renderer - * @param {Function} fnRender The function executed to render cells that match the render rules for this renderer. - */ - addWeekdayRenderer : function(weekday, fnRender) { - this._addRenderer(YAHOO.widget.Calendar.WEEKDAY,[weekday],fnRender); - }, - - // END RENDERER METHODS - - // BEGIN CSS METHODS - - /** - * Removes all styles from all body cells in the current calendar table. - * @method clearAllBodyCellStyles - * @param {style} style The CSS class name to remove from all calendar body cells - */ - clearAllBodyCellStyles : function(style) { - for (var c=0;c<this.cells.length;++c) { - YAHOO.util.Dom.removeClass(this.cells[c],style); - } - }, - - // END CSS METHODS - - // BEGIN GETTER/SETTER METHODS - /** - * Sets the calendar's month explicitly - * @method setMonth - * @param {Number} month The numeric month, from 0 (January) to 11 (December) - */ - setMonth : function(month) { - var cfgPageDate = YAHOO.widget.Calendar._DEFAULT_CONFIG.PAGEDATE.key; - var current = this.cfg.getProperty(cfgPageDate); - current.setMonth(parseInt(month, 10)); - this.cfg.setProperty(cfgPageDate, current); - }, - - /** - * Sets the calendar's year explicitly. - * @method setYear - * @param {Number} year The numeric 4-digit year - */ - setYear : function(year) { - var cfgPageDate = YAHOO.widget.Calendar._DEFAULT_CONFIG.PAGEDATE.key; - var current = this.cfg.getProperty(cfgPageDate); - current.setFullYear(parseInt(year, 10)); - this.cfg.setProperty(cfgPageDate, current); - }, - - /** - * Gets the list of currently selected dates from the calendar. - * @method getSelectedDates - * @return {Date[]} An array of currently selected JavaScript Date objects. - */ - getSelectedDates : function() { - var returnDates = []; - var selected = this.cfg.getProperty(YAHOO.widget.Calendar._DEFAULT_CONFIG.SELECTED.key); - - for (var d=0;d<selected.length;++d) { - var dateArray = selected[d]; - - var date = YAHOO.widget.DateMath.getDate(dateArray[0],dateArray[1]-1,dateArray[2]); - returnDates.push(date); - } - - returnDates.sort( function(a,b) { return a-b; } ); - return returnDates; - }, - - /// END GETTER/SETTER METHODS /// - - /** - * Hides the Calendar's outer container from view. - * @method hide - */ - hide : function() { - if (this.beforeHideEvent.fire()) { - this.oDomContainer.style.display = "none"; - this.hideEvent.fire(); - } - }, - - /** - * Shows the Calendar's outer container. - * @method show - */ - show : function() { - if (this.beforeShowEvent.fire()) { - this.oDomContainer.style.display = "block"; - this.showEvent.fire(); - } - }, - - /** - * Returns a string representing the current browser. - * @deprecated As of 2.3.0, environment information is available in YAHOO.env.ua - * @see YAHOO.env.ua - * @property browser - * @type String - */ - browser : (function() { - var ua = navigator.userAgent.toLowerCase(); - if (ua.indexOf('opera')!=-1) { // Opera (check first in case of spoof) - return 'opera'; - } else if (ua.indexOf('msie 7')!=-1) { // IE7 - return 'ie7'; - } else if (ua.indexOf('msie') !=-1) { // IE - return 'ie'; - } else if (ua.indexOf('safari')!=-1) { // Safari (check before Gecko because it includes "like Gecko") - return 'safari'; - } else if (ua.indexOf('gecko') != -1) { // Gecko - return 'gecko'; - } else { - return false; - } - })(), - /** - * Returns a string representation of the object. - * @method toString - * @return {String} A string representation of the Calendar object. - */ - toString : function() { - return "Calendar " + this.id; - } -}; - -/** -* @namespace YAHOO.widget -* @class Calendar_Core -* @extends YAHOO.widget.Calendar -* @deprecated The old Calendar_Core class is no longer necessary. -*/ -YAHOO.widget.Calendar_Core = YAHOO.widget.Calendar; - -YAHOO.widget.Cal_Core = YAHOO.widget.Calendar; - -/** -* YAHOO.widget.CalendarGroup is a special container class for YAHOO.widget.Calendar. This class facilitates -* the ability to have multi-page calendar views that share a single dataset and are -* dependent on each other. -* -* The calendar group instance will refer to each of its elements using a 0-based index. -* For example, to construct the placeholder for a calendar group widget with id "cal1" and -* containerId of "cal1Container", the markup would be as follows: -* <xmp> -* <div id="cal1Container_0"></div> -* <div id="cal1Container_1"></div> -* </xmp> -* The tables for the calendars ("cal1_0" and "cal1_1") will be inserted into those containers. -* -* <p> -* <strong>NOTE: As of 2.4.0, the constructor's ID argument is optional.</strong> -* The CalendarGroup can be constructed by simply providing a container ID string, -* or a reference to a container DIV HTMLElement (the element needs to exist -* in the document). -* -* E.g.: -* <xmp> -* var c = new YAHOO.widget.CalendarGroup("calContainer", configOptions); -* </xmp> -* or: -* <xmp> -* var containerDiv = YAHOO.util.Dom.get("calContainer"); -* var c = new YAHOO.widget.CalendarGroup(containerDiv, configOptions); -* </xmp> -* </p> -* <p> -* If not provided, the ID will be generated from the container DIV ID by adding an "_t" suffix. -* For example if an ID is not provided, and the container's ID is "calContainer", the CalendarGroup's ID will be set to "calContainer_t". -* </p> -* -* @namespace YAHOO.widget -* @class CalendarGroup -* @constructor -* @param {String} id optional The id of the table element that will represent the CalendarGroup widget. As of 2.4.0, this argument is optional. -* @param {String | HTMLElement} container The id of the container div element that will wrap the CalendarGroup table, or a reference to a DIV element which exists in the document. -* @param {Object} config optional The configuration object containing the initial configuration values for the CalendarGroup. -*/ -YAHOO.widget.CalendarGroup = function(id, containerId, config) { - if (arguments.length > 0) { - this.init.apply(this, arguments); - } -}; - -YAHOO.widget.CalendarGroup.prototype = { - - /** - * Initializes the calendar group. All subclasses must call this method in order for the - * group to be initialized properly. - * @method init - * @param {String} id optional The id of the table element that will represent the CalendarGroup widget. As of 2.4.0, this argument is optional. - * @param {String | HTMLElement} container The id of the container div element that will wrap the CalendarGroup table, or a reference to a DIV element which exists in the document. - * @param {Object} config optional The configuration object containing the initial configuration values for the CalendarGroup. - */ - init : function(id, container, config) { - - // Normalize 2.4.0, pre 2.4.0 args - var nArgs = this._parseArgs(arguments); - - id = nArgs.id; - container = nArgs.container; - config = nArgs.config; - - this.oDomContainer = YAHOO.util.Dom.get(container); - - if (!this.oDomContainer.id) { - this.oDomContainer.id = YAHOO.util.Dom.generateId(); - } - if (!id) { - id = this.oDomContainer.id + "_t"; - } - - /** - * The unique id associated with the CalendarGroup - * @property id - * @type String - */ - this.id = id; - - /** - * The unique id associated with the CalendarGroup container - * @property containerId - * @type String - */ - this.containerId = this.oDomContainer.id; - - this.initEvents(); - this.initStyles(); - - /** - * The collection of Calendar pages contained within the CalendarGroup - * @property pages - * @type YAHOO.widget.Calendar[] - */ - this.pages = []; - - YAHOO.util.Dom.addClass(this.oDomContainer, YAHOO.widget.CalendarGroup.CSS_CONTAINER); - YAHOO.util.Dom.addClass(this.oDomContainer, YAHOO.widget.CalendarGroup.CSS_MULTI_UP); - - /** - * The Config object used to hold the configuration variables for the CalendarGroup - * @property cfg - * @type YAHOO.util.Config - */ - this.cfg = new YAHOO.util.Config(this); - - /** - * The local object which contains the CalendarGroup's options - * @property Options - * @type Object - */ - this.Options = {}; - - /** - * The local object which contains the CalendarGroup's locale settings - * @property Locale - * @type Object - */ - this.Locale = {}; - - this.setupConfig(); - - if (config) { - this.cfg.applyConfig(config, true); - } - - this.cfg.fireQueue(); - - // OPERA HACK FOR MISWRAPPED FLOATS - if (YAHOO.env.ua.opera){ - this.renderEvent.subscribe(this._fixWidth, this, true); - this.showEvent.subscribe(this._fixWidth, this, true); - } - - }, - - setupConfig : function() { - - var defCfg = YAHOO.widget.CalendarGroup._DEFAULT_CONFIG; - - /** - * The number of pages to include in the CalendarGroup. This value can only be set once, in the CalendarGroup's constructor arguments. - * @config pages - * @type Number - * @default 2 - */ - this.cfg.addProperty(defCfg.PAGES.key, { value:defCfg.PAGES.value, validator:this.cfg.checkNumber, handler:this.configPages } ); - - /** - * The month/year representing the current visible Calendar date (mm/yyyy) - * @config pagedate - * @type String - * @default today's date - */ - this.cfg.addProperty(defCfg.PAGEDATE.key, { value:new Date(), handler:this.configPageDate } ); - - /** - * The date or range of dates representing the current Calendar selection - * @config selected - * @type String - * @default [] - */ - this.cfg.addProperty(defCfg.SELECTED.key, { value:[], handler:this.configSelected } ); - - /** - * The title to display above the CalendarGroup's month header - * @config title - * @type String - * @default "" - */ - this.cfg.addProperty(defCfg.TITLE.key, { value:defCfg.TITLE.value, handler:this.configTitle } ); - - /** - * Whether or not a close button should be displayed for this CalendarGroup - * @config close - * @type Boolean - * @default false - */ - this.cfg.addProperty(defCfg.CLOSE.key, { value:defCfg.CLOSE.value, handler:this.configClose } ); - - /** - * Whether or not an iframe shim should be placed under the Calendar to prevent select boxes from bleeding through in Internet Explorer 6 and below. - * This property is enabled by default for IE6 and below. It is disabled by default for other browsers for performance reasons, but can be - * enabled if required. - * - * @config iframe - * @type Boolean - * @default true for IE6 and below, false for all other browsers - */ - this.cfg.addProperty(defCfg.IFRAME.key, { value:defCfg.IFRAME.value, handler:this.configIframe, validator:this.cfg.checkBoolean } ); - - /** - * The minimum selectable date in the current Calendar (mm/dd/yyyy) - * @config mindate - * @type String - * @default null - */ - this.cfg.addProperty(defCfg.MINDATE.key, { value:defCfg.MINDATE.value, handler:this.delegateConfig } ); - - /** - * The maximum selectable date in the current Calendar (mm/dd/yyyy) - * @config maxdate - * @type String - * @default null - */ - this.cfg.addProperty(defCfg.MAXDATE.key, { value:defCfg.MAXDATE.value, handler:this.delegateConfig } ); - - // Options properties - - /** - * True if the Calendar should allow multiple selections. False by default. - * @config MULTI_SELECT - * @type Boolean - * @default false - */ - this.cfg.addProperty(defCfg.MULTI_SELECT.key, { value:defCfg.MULTI_SELECT.value, handler:this.delegateConfig, validator:this.cfg.checkBoolean } ); - - /** - * The weekday the week begins on. Default is 0 (Sunday). - * @config START_WEEKDAY - * @type number - * @default 0 - */ - this.cfg.addProperty(defCfg.START_WEEKDAY.key, { value:defCfg.START_WEEKDAY.value, handler:this.delegateConfig, validator:this.cfg.checkNumber } ); - - /** - * True if the Calendar should show weekday labels. True by default. - * @config SHOW_WEEKDAYS - * @type Boolean - * @default true - */ - this.cfg.addProperty(defCfg.SHOW_WEEKDAYS.key, { value:defCfg.SHOW_WEEKDAYS.value, handler:this.delegateConfig, validator:this.cfg.checkBoolean } ); - - /** - * True if the Calendar should show week row headers. False by default. - * @config SHOW_WEEK_HEADER - * @type Boolean - * @default false - */ - this.cfg.addProperty(defCfg.SHOW_WEEK_HEADER.key,{ value:defCfg.SHOW_WEEK_HEADER.value, handler:this.delegateConfig, validator:this.cfg.checkBoolean } ); - - /** - * True if the Calendar should show week row footers. False by default. - * @config SHOW_WEEK_FOOTER - * @type Boolean - * @default false - */ - this.cfg.addProperty(defCfg.SHOW_WEEK_FOOTER.key,{ value:defCfg.SHOW_WEEK_FOOTER.value, handler:this.delegateConfig, validator:this.cfg.checkBoolean } ); - - /** - * True if the Calendar should suppress weeks that are not a part of the current month. False by default. - * @config HIDE_BLANK_WEEKS - * @type Boolean - * @default false - */ - this.cfg.addProperty(defCfg.HIDE_BLANK_WEEKS.key,{ value:defCfg.HIDE_BLANK_WEEKS.value, handler:this.delegateConfig, validator:this.cfg.checkBoolean } ); - - /** - * The image that should be used for the left navigation arrow. - * @config NAV_ARROW_LEFT - * @type String - * @deprecated You can customize the image by overriding the default CSS class for the left arrow - "calnavleft" - * @default null - */ - this.cfg.addProperty(defCfg.NAV_ARROW_LEFT.key, { value:defCfg.NAV_ARROW_LEFT.value, handler:this.delegateConfig } ); - - /** - * The image that should be used for the right navigation arrow. - * @config NAV_ARROW_RIGHT - * @type String - * @deprecated You can customize the image by overriding the default CSS class for the right arrow - "calnavright" - * @default null - */ - this.cfg.addProperty(defCfg.NAV_ARROW_RIGHT.key, { value:defCfg.NAV_ARROW_RIGHT.value, handler:this.delegateConfig } ); - - // Locale properties - - /** - * The short month labels for the current locale. - * @config MONTHS_SHORT - * @type String[] - * @default ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] - */ - this.cfg.addProperty(defCfg.MONTHS_SHORT.key, { value:defCfg.MONTHS_SHORT.value, handler:this.delegateConfig } ); - - /** - * The long month labels for the current locale. - * @config MONTHS_LONG - * @type String[] - * @default ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" - */ - this.cfg.addProperty(defCfg.MONTHS_LONG.key, { value:defCfg.MONTHS_LONG.value, handler:this.delegateConfig } ); - - /** - * The 1-character weekday labels for the current locale. - * @config WEEKDAYS_1CHAR - * @type String[] - * @default ["S", "M", "T", "W", "T", "F", "S"] - */ - this.cfg.addProperty(defCfg.WEEKDAYS_1CHAR.key, { value:defCfg.WEEKDAYS_1CHAR.value, handler:this.delegateConfig } ); - - /** - * The short weekday labels for the current locale. - * @config WEEKDAYS_SHORT - * @type String[] - * @default ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"] - */ - this.cfg.addProperty(defCfg.WEEKDAYS_SHORT.key, { value:defCfg.WEEKDAYS_SHORT.value, handler:this.delegateConfig } ); - - /** - * The medium weekday labels for the current locale. - * @config WEEKDAYS_MEDIUM - * @type String[] - * @default ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"] - */ - this.cfg.addProperty(defCfg.WEEKDAYS_MEDIUM.key, { value:defCfg.WEEKDAYS_MEDIUM.value, handler:this.delegateConfig } ); - - /** - * The long weekday labels for the current locale. - * @config WEEKDAYS_LONG - * @type String[] - * @default ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"] - */ - this.cfg.addProperty(defCfg.WEEKDAYS_LONG.key, { value:defCfg.WEEKDAYS_LONG.value, handler:this.delegateConfig } ); - - /** - * The setting that determines which length of month labels should be used. Possible values are "short" and "long". - * @config LOCALE_MONTHS - * @type String - * @default "long" - */ - this.cfg.addProperty(defCfg.LOCALE_MONTHS.key, { value:defCfg.LOCALE_MONTHS.value, handler:this.delegateConfig } ); - - /** - * The setting that determines which length of weekday labels should be used. Possible values are "1char", "short", "medium", and "long". - * @config LOCALE_WEEKDAYS - * @type String - * @default "short" - */ - this.cfg.addProperty(defCfg.LOCALE_WEEKDAYS.key, { value:defCfg.LOCALE_WEEKDAYS.value, handler:this.delegateConfig } ); - - /** - * The value used to delimit individual dates in a date string passed to various Calendar functions. - * @config DATE_DELIMITER - * @type String - * @default "," - */ - this.cfg.addProperty(defCfg.DATE_DELIMITER.key, { value:defCfg.DATE_DELIMITER.value, handler:this.delegateConfig } ); - - /** - * The value used to delimit date fields in a date string passed to various Calendar functions. - * @config DATE_FIELD_DELIMITER - * @type String - * @default "/" - */ - this.cfg.addProperty(defCfg.DATE_FIELD_DELIMITER.key,{ value:defCfg.DATE_FIELD_DELIMITER.value, handler:this.delegateConfig } ); - - /** - * The value used to delimit date ranges in a date string passed to various Calendar functions. - * @config DATE_RANGE_DELIMITER - * @type String - * @default "-" - */ - this.cfg.addProperty(defCfg.DATE_RANGE_DELIMITER.key,{ value:defCfg.DATE_RANGE_DELIMITER.value, handler:this.delegateConfig } ); - - /** - * The position of the month in a month/year date string - * @config MY_MONTH_POSITION - * @type Number - * @default 1 - */ - this.cfg.addProperty(defCfg.MY_MONTH_POSITION.key, { value:defCfg.MY_MONTH_POSITION.value, handler:this.delegateConfig, validator:this.cfg.checkNumber } ); - - /** - * The position of the year in a month/year date string - * @config MY_YEAR_POSITION - * @type Number - * @default 2 - */ - this.cfg.addProperty(defCfg.MY_YEAR_POSITION.key, { value:defCfg.MY_YEAR_POSITION.value, handler:this.delegateConfig, validator:this.cfg.checkNumber } ); - - /** - * The position of the month in a month/day date string - * @config MD_MONTH_POSITION - * @type Number - * @default 1 - */ - this.cfg.addProperty(defCfg.MD_MONTH_POSITION.key, { value:defCfg.MD_MONTH_POSITION.value, handler:this.delegateConfig, validator:this.cfg.checkNumber } ); - - /** - * The position of the day in a month/year date string - * @config MD_DAY_POSITION - * @type Number - * @default 2 - */ - this.cfg.addProperty(defCfg.MD_DAY_POSITION.key, { value:defCfg.MD_DAY_POSITION.value, handler:this.delegateConfig, validator:this.cfg.checkNumber } ); - - /** - * The position of the month in a month/day/year date string - * @config MDY_MONTH_POSITION - * @type Number - * @default 1 - */ - this.cfg.addProperty(defCfg.MDY_MONTH_POSITION.key, { value:defCfg.MDY_MONTH_POSITION.value, handler:this.delegateConfig, validator:this.cfg.checkNumber } ); - - /** - * The position of the day in a month/day/year date string - * @config MDY_DAY_POSITION - * @type Number - * @default 2 - */ - this.cfg.addProperty(defCfg.MDY_DAY_POSITION.key, { value:defCfg.MDY_DAY_POSITION.value, handler:this.delegateConfig, validator:this.cfg.checkNumber } ); - - /** - * The position of the year in a month/day/year date string - * @config MDY_YEAR_POSITION - * @type Number - * @default 3 - */ - this.cfg.addProperty(defCfg.MDY_YEAR_POSITION.key, { value:defCfg.MDY_YEAR_POSITION.value, handler:this.delegateConfig, validator:this.cfg.checkNumber } ); - - /** - * The position of the month in the month year label string used as the Calendar header - * @config MY_LABEL_MONTH_POSITION - * @type Number - * @default 1 - */ - this.cfg.addProperty(defCfg.MY_LABEL_MONTH_POSITION.key, { value:defCfg.MY_LABEL_MONTH_POSITION.value, handler:this.delegateConfig, validator:this.cfg.checkNumber } ); - - /** - * The position of the year in the month year label string used as the Calendar header - * @config MY_LABEL_YEAR_POSITION - * @type Number - * @default 2 - */ - this.cfg.addProperty(defCfg.MY_LABEL_YEAR_POSITION.key, { value:defCfg.MY_LABEL_YEAR_POSITION.value, handler:this.delegateConfig, validator:this.cfg.checkNumber } ); - - /** - * The suffix used after the month when rendering the Calendar header - * @config MY_LABEL_MONTH_SUFFIX - * @type String - * @default " " - */ - this.cfg.addProperty(defCfg.MY_LABEL_MONTH_SUFFIX.key, { value:defCfg.MY_LABEL_MONTH_SUFFIX.value, handler:this.delegateConfig } ); - - /** - * The suffix used after the year when rendering the Calendar header - * @config MY_LABEL_YEAR_SUFFIX - * @type String - * @default "" - */ - this.cfg.addProperty(defCfg.MY_LABEL_YEAR_SUFFIX.key, { value:defCfg.MY_LABEL_YEAR_SUFFIX.value, handler:this.delegateConfig } ); - - /** - * Configuration for the Month Year Navigation UI. By default it is disabled - * @config NAV - * @type Object - * @default null - */ - this.cfg.addProperty(defCfg.NAV.key, { value:defCfg.NAV.value, handler:this.configNavigator } ); - }, - - /** - * Initializes CalendarGroup's built-in CustomEvents - * @method initEvents - */ - initEvents : function() { - var me = this; - var strEvent = "Event"; - - /** - * Proxy subscriber to subscribe to the CalendarGroup's child Calendars' CustomEvents - * @method sub - * @private - * @param {Function} fn The function to subscribe to this CustomEvent - * @param {Object} obj The CustomEvent's scope object - * @param {Boolean} bOverride Whether or not to apply scope correction - */ - var sub = function(fn, obj, bOverride) { - for (var p=0;p<me.pages.length;++p) { - var cal = me.pages[p]; - cal[this.type + strEvent].subscribe(fn, obj, bOverride); - } - }; - - /** - * Proxy unsubscriber to unsubscribe from the CalendarGroup's child Calendars' CustomEvents - * @method unsub - * @private - * @param {Function} fn The function to subscribe to this CustomEvent - * @param {Object} obj The CustomEvent's scope object - */ - var unsub = function(fn, obj) { - for (var p=0;p<me.pages.length;++p) { - var cal = me.pages[p]; - cal[this.type + strEvent].unsubscribe(fn, obj); - } - }; - - var defEvents = YAHOO.widget.Calendar._EVENT_TYPES; - - /** - * Fired before a selection is made - * @event beforeSelectEvent - */ - this.beforeSelectEvent = new YAHOO.util.CustomEvent(defEvents.BEFORE_SELECT); - this.beforeSelectEvent.subscribe = sub; this.beforeSelectEvent.unsubscribe = unsub; - - /** - * Fired when a selection is made - * @event selectEvent - * @param {Array} Array of Date field arrays in the format [YYYY, MM, DD]. - */ - this.selectEvent = new YAHOO.util.CustomEvent(defEvents.SELECT); - this.selectEvent.subscribe = sub; this.selectEvent.unsubscribe = unsub; - - /** - * Fired before a selection is made - * @event beforeDeselectEvent - */ - this.beforeDeselectEvent = new YAHOO.util.CustomEvent(defEvents.BEFORE_DESELECT); - this.beforeDeselectEvent.subscribe = sub; this.beforeDeselectEvent.unsubscribe = unsub; - - /** - * Fired when a selection is made - * @event deselectEvent - * @param {Array} Array of Date field arrays in the format [YYYY, MM, DD]. - */ - this.deselectEvent = new YAHOO.util.CustomEvent(defEvents.DESELECT); - this.deselectEvent.subscribe = sub; this.deselectEvent.unsubscribe = unsub; - - /** - * Fired when the Calendar page is changed - * @event changePageEvent - */ - this.changePageEvent = new YAHOO.util.CustomEvent(defEvents.CHANGE_PAGE); - this.changePageEvent.subscribe = sub; this.changePageEvent.unsubscribe = unsub; - - /** - * Fired before the Calendar is rendered - * @event beforeRenderEvent - */ - this.beforeRenderEvent = new YAHOO.util.CustomEvent(defEvents.BEFORE_RENDER); - this.beforeRenderEvent.subscribe = sub; this.beforeRenderEvent.unsubscribe = unsub; - - /** - * Fired when the Calendar is rendered - * @event renderEvent - */ - this.renderEvent = new YAHOO.util.CustomEvent(defEvents.RENDER); - this.renderEvent.subscribe = sub; this.renderEvent.unsubscribe = unsub; - - /** - * Fired when the Calendar is reset - * @event resetEvent - */ - this.resetEvent = new YAHOO.util.CustomEvent(defEvents.RESET); - this.resetEvent.subscribe = sub; this.resetEvent.unsubscribe = unsub; - - /** - * Fired when the Calendar is cleared - * @event clearEvent - */ - this.clearEvent = new YAHOO.util.CustomEvent(defEvents.CLEAR); - this.clearEvent.subscribe = sub; this.clearEvent.unsubscribe = unsub; - - /** - * Fired just before the CalendarGroup is to be shown - * @event beforeShowEvent - */ - this.beforeShowEvent = new YAHOO.util.CustomEvent(defEvents.BEFORE_SHOW); - - /** - * Fired after the CalendarGroup is shown - * @event showEvent - */ - this.showEvent = new YAHOO.util.CustomEvent(defEvents.SHOW); - - /** - * Fired just before the CalendarGroup is to be hidden - * @event beforeHideEvent - */ - this.beforeHideEvent = new YAHOO.util.CustomEvent(defEvents.BEFORE_HIDE); - - /** - * Fired after the CalendarGroup is hidden - * @event hideEvent - */ - this.hideEvent = new YAHOO.util.CustomEvent(defEvents.HIDE); - - /** - * Fired just before the CalendarNavigator is to be shown - * @event beforeShowNavEvent - */ - this.beforeShowNavEvent = new YAHOO.util.CustomEvent(defEvents.BEFORE_SHOW_NAV); - - /** - * Fired after the CalendarNavigator is shown - * @event showNavEvent - */ - this.showNavEvent = new YAHOO.util.CustomEvent(defEvents.SHOW_NAV); - - /** - * Fired just before the CalendarNavigator is to be hidden - * @event beforeHideNavEvent - */ - this.beforeHideNavEvent = new YAHOO.util.CustomEvent(defEvents.BEFORE_HIDE_NAV); - - /** - * Fired after the CalendarNavigator is hidden - * @event hideNavEvent - */ - this.hideNavEvent = new YAHOO.util.CustomEvent(defEvents.HIDE_NAV); - - /** - * Fired just before the CalendarNavigator is to be rendered - * @event beforeRenderNavEvent - */ - this.beforeRenderNavEvent = new YAHOO.util.CustomEvent(defEvents.BEFORE_RENDER_NAV); - - /** - * Fired after the CalendarNavigator is rendered - * @event renderNavEvent - */ - this.renderNavEvent = new YAHOO.util.CustomEvent(defEvents.RENDER_NAV); - }, - - /** - * The default Config handler for the "pages" property - * @method configPages - * @param {String} type The CustomEvent type (usually the property name) - * @param {Object[]} args The CustomEvent arguments. For configuration handlers, args[0] will equal the newly applied value for the property. - * @param {Object} obj The scope object. For configuration handlers, this will usually equal the owner. - */ - configPages : function(type, args, obj) { - var pageCount = args[0]; - - var cfgPageDate = YAHOO.widget.CalendarGroup._DEFAULT_CONFIG.PAGEDATE.key; - - // Define literals outside loop - var sep = "_"; - var groupCalClass = "groupcal"; - - var firstClass = "first-of-type"; - var lastClass = "last-of-type"; - - for (var p=0;p<pageCount;++p) { - var calId = this.id + sep + p; - var calContainerId = this.containerId + sep + p; - - var childConfig = this.cfg.getConfig(); - childConfig.close = false; - childConfig.title = false; - childConfig.navigator = null; - - var cal = this.constructChild(calId, calContainerId, childConfig); - var caldate = cal.cfg.getProperty(cfgPageDate); - this._setMonthOnDate(caldate, caldate.getMonth() + p); - cal.cfg.setProperty(cfgPageDate, caldate); - - YAHOO.util.Dom.removeClass(cal.oDomContainer, this.Style.CSS_SINGLE); - YAHOO.util.Dom.addClass(cal.oDomContainer, groupCalClass); - - if (p===0) { - YAHOO.util.Dom.addClass(cal.oDomContainer, firstClass); - } - - if (p==(pageCount-1)) { - YAHOO.util.Dom.addClass(cal.oDomContainer, lastClass); - } - - cal.parent = this; - cal.index = p; - - this.pages[this.pages.length] = cal; - } - }, - - /** - * The default Config handler for the "pagedate" property - * @method configPageDate - * @param {String} type The CustomEvent type (usually the property name) - * @param {Object[]} args The CustomEvent arguments. For configuration handlers, args[0] will equal the newly applied value for the property. - * @param {Object} obj The scope object. For configuration handlers, this will usually equal the owner. - */ - configPageDate : function(type, args, obj) { - var val = args[0]; - var firstPageDate; - - var cfgPageDate = YAHOO.widget.CalendarGroup._DEFAULT_CONFIG.PAGEDATE.key; - - for (var p=0;p<this.pages.length;++p) { - var cal = this.pages[p]; - if (p === 0) { - firstPageDate = cal._parsePageDate(val); - cal.cfg.setProperty(cfgPageDate, firstPageDate); - } else { - var pageDate = new Date(firstPageDate); - this._setMonthOnDate(pageDate, pageDate.getMonth() + p); - cal.cfg.setProperty(cfgPageDate, pageDate); - } - } - }, - - /** - * The default Config handler for the CalendarGroup "selected" property - * @method configSelected - * @param {String} type The CustomEvent type (usually the property name) - * @param {Object[]} args The CustomEvent arguments. For configuration handlers, args[0] will equal the newly applied value for the property. - * @param {Object} obj The scope object. For configuration handlers, this will usually equal the owner. - */ - configSelected : function(type, args, obj) { - var cfgSelected = YAHOO.widget.CalendarGroup._DEFAULT_CONFIG.SELECTED.key; - this.delegateConfig(type, args, obj); - var selected = (this.pages.length > 0) ? this.pages[0].cfg.getProperty(cfgSelected) : []; - this.cfg.setProperty(cfgSelected, selected, true); - }, - - - /** - * Delegates a configuration property to the CustomEvents associated with the CalendarGroup's children - * @method delegateConfig - * @param {String} type The CustomEvent type (usually the property name) - * @param {Object[]} args The CustomEvent arguments. For configuration handlers, args[0] will equal the newly applied value for the property. - * @param {Object} obj The scope object. For configuration handlers, this will usually equal the owner. - */ - delegateConfig : function(type, args, obj) { - var val = args[0]; - var cal; - - for (var p=0;p<this.pages.length;p++) { - cal = this.pages[p]; - cal.cfg.setProperty(type, val); - } - }, - - /** - * Adds a function to all child Calendars within this CalendarGroup. - * @method setChildFunction - * @param {String} fnName The name of the function - * @param {Function} fn The function to apply to each Calendar page object - */ - setChildFunction : function(fnName, fn) { - var pageCount = this.cfg.getProperty(YAHOO.widget.CalendarGroup._DEFAULT_CONFIG.PAGES.key); - - for (var p=0;p<pageCount;++p) { - this.pages[p][fnName] = fn; - } - }, - - /** - * Calls a function within all child Calendars within this CalendarGroup. - * @method callChildFunction - * @param {String} fnName The name of the function - * @param {Array} args The arguments to pass to the function - */ - callChildFunction : function(fnName, args) { - var pageCount = this.cfg.getProperty(YAHOO.widget.CalendarGroup._DEFAULT_CONFIG.PAGES.key); - - for (var p=0;p<pageCount;++p) { - var page = this.pages[p]; - if (page[fnName]) { - var fn = page[fnName]; - fn.call(page, args); - } - } - }, - - /** - * Constructs a child calendar. This method can be overridden if a subclassed version of the default - * calendar is to be used. - * @method constructChild - * @param {String} id The id of the table element that will represent the calendar widget - * @param {String} containerId The id of the container div element that will wrap the calendar table - * @param {Object} config The configuration object containing the Calendar's arguments - * @return {YAHOO.widget.Calendar} The YAHOO.widget.Calendar instance that is constructed - */ - constructChild : function(id,containerId,config) { - var container = document.getElementById(containerId); - if (! container) { - container = document.createElement("div"); - container.id = containerId; - this.oDomContainer.appendChild(container); - } - return new YAHOO.widget.Calendar(id,containerId,config); - }, - - /** - * Sets the calendar group's month explicitly. This month will be set into the first - * page of the multi-page calendar, and all other months will be iterated appropriately. - * @method setMonth - * @param {Number} month The numeric month, from 0 (January) to 11 (December) - */ - setMonth : function(month) { - month = parseInt(month, 10); - var currYear; - - var cfgPageDate = YAHOO.widget.CalendarGroup._DEFAULT_CONFIG.PAGEDATE.key; - - for (var p=0; p<this.pages.length; ++p) { - var cal = this.pages[p]; - var pageDate = cal.cfg.getProperty(cfgPageDate); - if (p === 0) { - currYear = pageDate.getFullYear(); - } else { - pageDate.setFullYear(currYear); - } - this._setMonthOnDate(pageDate, month+p); - cal.cfg.setProperty(cfgPageDate, pageDate); - } - }, - - /** - * Sets the calendar group's year explicitly. This year will be set into the first - * page of the multi-page calendar, and all other months will be iterated appropriately. - * @method setYear - * @param {Number} year The numeric 4-digit year - */ - setYear : function(year) { - - var cfgPageDate = YAHOO.widget.CalendarGroup._DEFAULT_CONFIG.PAGEDATE.key; - - year = parseInt(year, 10); - for (var p=0;p<this.pages.length;++p) { - var cal = this.pages[p]; - var pageDate = cal.cfg.getProperty(cfgPageDate); - - if ((pageDate.getMonth()+1) == 1 && p>0) { - year+=1; - } - cal.setYear(year); - } - }, - - /** - * Calls the render function of all child calendars within the group. - * @method render - */ - render : function() { - this.renderHeader(); - for (var p=0;p<this.pages.length;++p) { - var cal = this.pages[p]; - cal.render(); - } - this.renderFooter(); - }, - - /** - * Selects a date or a collection of dates on the current calendar. This method, by default, - * does not call the render method explicitly. Once selection has completed, render must be - * called for the changes to be reflected visually. - * @method select - * @param {String/Date/Date[]} date The date string of dates to select in the current calendar. Valid formats are - * individual date(s) (12/24/2005,12/26/2005) or date range(s) (12/24/2005-1/1/2006). - * Multiple comma-delimited dates can also be passed to this method (12/24/2005,12/11/2005-12/13/2005). - * This method can also take a JavaScript Date object or an array of Date objects. - * @return {Date[]} Array of JavaScript Date objects representing all individual dates that are currently selected. - */ - select : function(date) { - for (var p=0;p<this.pages.length;++p) { - var cal = this.pages[p]; - cal.select(date); - } - return this.getSelectedDates(); - }, - - /** - * Selects dates in the CalendarGroup based on the cell index provided. This method is used to select cells without having to do a full render. The selected style is applied to the cells directly. - * The value of the MULTI_SELECT Configuration attribute will determine the set of dates which get selected. - * <ul> - * <li>If MULTI_SELECT is false, selectCell will select the cell at the specified index for only the last displayed Calendar page.</li> - * <li>If MULTI_SELECT is true, selectCell will select the cell at the specified index, on each displayed Calendar page.</li> - * </ul> - * @method selectCell - * @param {Number} cellIndex The index of the cell to be selected. - * @return {Date[]} Array of JavaScript Date objects representing all individual dates that are currently selected. - */ - selectCell : function(cellIndex) { - for (var p=0;p<this.pages.length;++p) { - var cal = this.pages[p]; - cal.selectCell(cellIndex); - } - return this.getSelectedDates(); - }, - - /** - * Deselects a date or a collection of dates on the current calendar. This method, by default, - * does not call the render method explicitly. Once deselection has completed, render must be - * called for the changes to be reflected visually. - * @method deselect - * @param {String/Date/Date[]} date The date string of dates to deselect in the current calendar. Valid formats are - * individual date(s) (12/24/2005,12/26/2005) or date range(s) (12/24/2005-1/1/2006). - * Multiple comma-delimited dates can also be passed to this method (12/24/2005,12/11/2005-12/13/2005). - * This method can also take a JavaScript Date object or an array of Date objects. - * @return {Date[]} Array of JavaScript Date objects representing all individual dates that are currently selected. - */ - deselect : function(date) { - for (var p=0;p<this.pages.length;++p) { - var cal = this.pages[p]; - cal.deselect(date); - } - return this.getSelectedDates(); - }, - - /** - * Deselects all dates on the current calendar. - * @method deselectAll - * @return {Date[]} Array of JavaScript Date objects representing all individual dates that are currently selected. - * Assuming that this function executes properly, the return value should be an empty array. - * However, the empty array is returned for the sake of being able to check the selection status - * of the calendar. - */ - deselectAll : function() { - for (var p=0;p<this.pages.length;++p) { - var cal = this.pages[p]; - cal.deselectAll(); - } - return this.getSelectedDates(); - }, - - /** - * Deselects dates in the CalendarGroup based on the cell index provided. This method is used to select cells without having to do a full render. The selected style is applied to the cells directly. - * deselectCell will deselect the cell at the specified index on each displayed Calendar page. - * - * @method deselectCell - * @param {Number} cellIndex The index of the cell to deselect. - * @return {Date[]} Array of JavaScript Date objects representing all individual dates that are currently selected. - */ - deselectCell : function(cellIndex) { - for (var p=0;p<this.pages.length;++p) { - var cal = this.pages[p]; - cal.deselectCell(cellIndex); - } - return this.getSelectedDates(); - }, - - /** - * Resets the calendar widget to the originally selected month and year, and - * sets the calendar to the initial selection(s). - * @method reset - */ - reset : function() { - for (var p=0;p<this.pages.length;++p) { - var cal = this.pages[p]; - cal.reset(); - } - }, - - /** - * Clears the selected dates in the current calendar widget and sets the calendar - * to the current month and year. - * @method clear - */ - clear : function() { - for (var p=0;p<this.pages.length;++p) { - var cal = this.pages[p]; - cal.clear(); - } - }, - - /** - * Navigates to the next month page in the calendar widget. - * @method nextMonth - */ - nextMonth : function() { - for (var p=0;p<this.pages.length;++p) { - var cal = this.pages[p]; - cal.nextMonth(); - } - }, - - /** - * Navigates to the previous month page in the calendar widget. - * @method previousMonth - */ - previousMonth : function() { - for (var p=this.pages.length-1;p>=0;--p) { - var cal = this.pages[p]; - cal.previousMonth(); - } - }, - - /** - * Navigates to the next year in the currently selected month in the calendar widget. - * @method nextYear - */ - nextYear : function() { - for (var p=0;p<this.pages.length;++p) { - var cal = this.pages[p]; - cal.nextYear(); - } - }, - - /** - * Navigates to the previous year in the currently selected month in the calendar widget. - * @method previousYear - */ - previousYear : function() { - for (var p=0;p<this.pages.length;++p) { - var cal = this.pages[p]; - cal.previousYear(); - } - }, - - /** - * Gets the list of currently selected dates from the calendar. - * @return An array of currently selected JavaScript Date objects. - * @type Date[] - */ - getSelectedDates : function() { - var returnDates = []; - var selected = this.cfg.getProperty(YAHOO.widget.CalendarGroup._DEFAULT_CONFIG.SELECTED.key); - for (var d=0;d<selected.length;++d) { - var dateArray = selected[d]; - - var date = YAHOO.widget.DateMath.getDate(dateArray[0],dateArray[1]-1,dateArray[2]); - returnDates.push(date); - } - - returnDates.sort( function(a,b) { return a-b; } ); - return returnDates; - }, - - /** - * Adds a renderer to the render stack. The function reference passed to this method will be executed - * when a date cell matches the conditions specified in the date string for this renderer. - * @method addRenderer - * @param {String} sDates A date string to associate with the specified renderer. Valid formats - * include date (12/24/2005), month/day (12/24), and range (12/1/2004-1/1/2005) - * @param {Function} fnRender The function executed to render cells that match the render rules for this renderer. - */ - addRenderer : function(sDates, fnRender) { - for (var p=0;p<this.pages.length;++p) { - var cal = this.pages[p]; - cal.addRenderer(sDates, fnRender); - } - }, - - /** - * Adds a month to the render stack. The function reference passed to this method will be executed - * when a date cell matches the month passed to this method. - * @method addMonthRenderer - * @param {Number} month The month (1-12) to associate with this renderer - * @param {Function} fnRender The function executed to render cells that match the render rules for this renderer. - */ - addMonthRenderer : function(month, fnRender) { - for (var p=0;p<this.pages.length;++p) { - var cal = this.pages[p]; - cal.addMonthRenderer(month, fnRender); - } - }, - - /** - * Adds a weekday to the render stack. The function reference passed to this method will be executed - * when a date cell matches the weekday passed to this method. - * @method addWeekdayRenderer - * @param {Number} weekday The weekday (1-7) to associate with this renderer. 1=Sunday, 2=Monday etc. - * @param {Function} fnRender The function executed to render cells that match the render rules for this renderer. - */ - addWeekdayRenderer : function(weekday, fnRender) { - for (var p=0;p<this.pages.length;++p) { - var cal = this.pages[p]; - cal.addWeekdayRenderer(weekday, fnRender); - } - }, - - /** - * Removes all custom renderers added to the CalendarGroup through the addRenderer, addMonthRenderer and - * addWeekRenderer methods. CalendarGroup's render method needs to be called to after removing renderers - * to see the changes applied. - * - * @method removeRenderers - */ - removeRenderers : function() { - this.callChildFunction("removeRenderers"); - }, - - /** - * Renders the header for the CalendarGroup. - * @method renderHeader - */ - renderHeader : function() { - // EMPTY DEFAULT IMPL - }, - - /** - * Renders a footer for the 2-up calendar container. By default, this method is - * unimplemented. - * @method renderFooter - */ - renderFooter : function() { - // EMPTY DEFAULT IMPL - }, - - /** - * Adds the designated number of months to the current calendar month, and sets the current - * calendar page date to the new month. - * @method addMonths - * @param {Number} count The number of months to add to the current calendar - */ - addMonths : function(count) { - this.callChildFunction("addMonths", count); - }, - - /** - * Subtracts the designated number of months from the current calendar month, and sets the current - * calendar page date to the new month. - * @method subtractMonths - * @param {Number} count The number of months to subtract from the current calendar - */ - subtractMonths : function(count) { - this.callChildFunction("subtractMonths", count); - }, - - /** - * Adds the designated number of years to the current calendar, and sets the current - * calendar page date to the new month. - * @method addYears - * @param {Number} count The number of years to add to the current calendar - */ - addYears : function(count) { - this.callChildFunction("addYears", count); - }, - - /** - * Subtcats the designated number of years from the current calendar, and sets the current - * calendar page date to the new month. - * @method subtractYears - * @param {Number} count The number of years to subtract from the current calendar - */ - subtractYears : function(count) { - this.callChildFunction("subtractYears", count); - }, - - /** - * Returns the Calendar page instance which has a pagedate (month/year) matching the given date. - * Returns null if no match is found. - * - * @method getCalendarPage - * @param {Date} date The JavaScript Date object for which a Calendar page is to be found. - * @return {Calendar} The Calendar page instance representing the month to which the date - * belongs. - */ - getCalendarPage : function(date) { - var cal = null; - if (date) { - var y = date.getFullYear(), - m = date.getMonth(); - - var pages = this.pages; - for (var i = 0; i < pages.length; ++i) { - var pageDate = pages[i].cfg.getProperty("pagedate"); - if (pageDate.getFullYear() === y && pageDate.getMonth() === m) { - cal = pages[i]; - break; - } - } - } - return cal; - }, - - /** - * Sets the month on a Date object, taking into account year rollover if the month is less than 0 or greater than 11. - * The Date object passed in is modified. It should be cloned before passing it into this method if the original value needs to be maintained - * @method _setMonthOnDate - * @private - * @param {Date} date The Date object on which to set the month index - * @param {Number} iMonth The month index to set - */ - _setMonthOnDate : function(date, iMonth) { - // Bug in Safari 1.3, 2.0 (WebKit build < 420), Date.setMonth does not work consistently if iMonth is not 0-11 - if (YAHOO.env.ua.webkit && YAHOO.env.ua.webkit < 420 && (iMonth < 0 || iMonth > 11)) { - var DM = YAHOO.widget.DateMath; - var newDate = DM.add(date, DM.MONTH, iMonth-date.getMonth()); - date.setTime(newDate.getTime()); - } else { - date.setMonth(iMonth); - } - }, - - /** - * Fixes the width of the CalendarGroup container element, to account for miswrapped floats - * @method _fixWidth - * @private - */ - _fixWidth : function() { - var w = 0; - for (var p=0;p<this.pages.length;++p) { - var cal = this.pages[p]; - w += cal.oDomContainer.offsetWidth; - } - if (w > 0) { - this.oDomContainer.style.width = w + "px"; - } - }, - - /** - * Returns a string representation of the object. - * @method toString - * @return {String} A string representation of the CalendarGroup object. - */ - toString : function() { - return "CalendarGroup " + this.id; - } -}; - -/** -* CSS class representing the container for the calendar -* @property YAHOO.widget.CalendarGroup.CSS_CONTAINER -* @static -* @final -* @type String -*/ -YAHOO.widget.CalendarGroup.CSS_CONTAINER = "yui-calcontainer"; - -/** -* CSS class representing the container for the calendar -* @property YAHOO.widget.CalendarGroup.CSS_MULTI_UP -* @static -* @final -* @type String -*/ -YAHOO.widget.CalendarGroup.CSS_MULTI_UP = "multi"; - -/** -* CSS class representing the title for the 2-up calendar -* @property YAHOO.widget.CalendarGroup.CSS_2UPTITLE -* @static -* @final -* @type String -*/ -YAHOO.widget.CalendarGroup.CSS_2UPTITLE = "title"; - -/** -* CSS class representing the close icon for the 2-up calendar -* @property YAHOO.widget.CalendarGroup.CSS_2UPCLOSE -* @static -* @final -* @deprecated Along with Calendar.IMG_ROOT and NAV_ARROW_LEFT, NAV_ARROW_RIGHT configuration properties. -* Calendar's <a href="YAHOO.widget.Calendar.html#Style.CSS_CLOSE">Style.CSS_CLOSE</a> property now represents the CSS class used to render the close icon -* @type String -*/ -YAHOO.widget.CalendarGroup.CSS_2UPCLOSE = "close-icon"; - -YAHOO.lang.augmentProto(YAHOO.widget.CalendarGroup, YAHOO.widget.Calendar, "buildDayLabel", - "buildMonthLabel", - "renderOutOfBoundsDate", - "renderRowHeader", - "renderRowFooter", - "renderCellDefault", - "styleCellDefault", - "renderCellStyleHighlight1", - "renderCellStyleHighlight2", - "renderCellStyleHighlight3", - "renderCellStyleHighlight4", - "renderCellStyleToday", - "renderCellStyleSelected", - "renderCellNotThisMonth", - "renderBodyCellRestricted", - "initStyles", - "configTitle", - "configClose", - "configIframe", - "configNavigator", - "createTitleBar", - "createCloseButton", - "removeTitleBar", - "removeCloseButton", - "hide", - "show", - "toDate", - "_parseArgs", - "browser"); - -/** -* The set of default Config property keys and values for the CalendarGroup -* @property YAHOO.widget.CalendarGroup._DEFAULT_CONFIG -* @final -* @static -* @private -* @type Object -*/ -YAHOO.widget.CalendarGroup._DEFAULT_CONFIG = YAHOO.widget.Calendar._DEFAULT_CONFIG; -YAHOO.widget.CalendarGroup._DEFAULT_CONFIG.PAGES = {key:"pages", value:2}; - -YAHOO.widget.CalGrp = YAHOO.widget.CalendarGroup; - -/** -* @class YAHOO.widget.Calendar2up -* @extends YAHOO.widget.CalendarGroup -* @deprecated The old Calendar2up class is no longer necessary, since CalendarGroup renders in a 2up view by default. -*/ -YAHOO.widget.Calendar2up = function(id, containerId, config) { - this.init(id, containerId, config); -}; - -YAHOO.extend(YAHOO.widget.Calendar2up, YAHOO.widget.CalendarGroup); - -/** -* @deprecated The old Calendar2up class is no longer necessary, since CalendarGroup renders in a 2up view by default. -*/ -YAHOO.widget.Cal2up = YAHOO.widget.Calendar2up; - -/** - * The CalendarNavigator is used along with a Calendar/CalendarGroup to - * provide a Month/Year popup navigation control, allowing the user to navigate - * to a specific month/year in the Calendar/CalendarGroup without having to - * scroll through months sequentially - * - * @namespace YAHOO.widget - * @class CalendarNavigator - * @constructor - * @param {Calendar|CalendarGroup} cal The instance of the Calendar or CalendarGroup to which this CalendarNavigator should be attached. - */ -YAHOO.widget.CalendarNavigator = function(cal) { - this.init(cal); -}; - -(function() { - // Setup static properties (inside anon fn, so that we can use shortcuts) - var CN = YAHOO.widget.CalendarNavigator; - - /** - * YAHOO.widget.CalendarNavigator.CLASSES contains constants - * for the class values applied to the CalendarNaviatgator's - * DOM elements - * @property YAHOO.widget.CalendarNavigator.CLASSES - * @type Object - * @static - */ - CN.CLASSES = { - /** - * Class applied to the Calendar Navigator's bounding box - * @property YAHOO.widget.CalendarNavigator.CLASSES.NAV - * @type String - * @static - */ - NAV :"yui-cal-nav", - /** - * Class applied to the Calendar/CalendarGroup's bounding box to indicate - * the Navigator is currently visible - * @property YAHOO.widget.CalendarNavigator.CLASSES.NAV_VISIBLE - * @type String - * @static - */ - NAV_VISIBLE: "yui-cal-nav-visible", - /** - * Class applied to the Navigator mask's bounding box - * @property YAHOO.widget.CalendarNavigator.CLASSES.MASK - * @type String - * @static - */ - MASK : "yui-cal-nav-mask", - /** - * Class applied to the year label/control bounding box - * @property YAHOO.widget.CalendarNavigator.CLASSES.YEAR - * @type String - * @static - */ - YEAR : "yui-cal-nav-y", - /** - * Class applied to the month label/control bounding box - * @property YAHOO.widget.CalendarNavigator.CLASSES.MONTH - * @type String - * @static - */ - MONTH : "yui-cal-nav-m", - /** - * Class applied to the submit/cancel button's bounding box - * @property YAHOO.widget.CalendarNavigator.CLASSES.BUTTONS - * @type String - * @static - */ - BUTTONS : "yui-cal-nav-b", - /** - * Class applied to buttons wrapping element - * @property YAHOO.widget.CalendarNavigator.CLASSES.BUTTON - * @type String - * @static - */ - BUTTON : "yui-cal-nav-btn", - /** - * Class applied to the validation error area's bounding box - * @property YAHOO.widget.CalendarNavigator.CLASSES.ERROR - * @type String - * @static - */ - ERROR : "yui-cal-nav-e", - /** - * Class applied to the year input control - * @property YAHOO.widget.CalendarNavigator.CLASSES.YEAR_CTRL - * @type String - * @static - */ - YEAR_CTRL : "yui-cal-nav-yc", - /** - * Class applied to the month input control - * @property YAHOO.widget.CalendarNavigator.CLASSES.MONTH_CTRL - * @type String - * @static - */ - MONTH_CTRL : "yui-cal-nav-mc", - /** - * Class applied to controls with invalid data (e.g. a year input field with invalid an year) - * @property YAHOO.widget.CalendarNavigator.CLASSES.INVALID - * @type String - * @static - */ - INVALID : "yui-invalid", - /** - * Class applied to default controls - * @property YAHOO.widget.CalendarNavigator.CLASSES.DEFAULT - * @type String - * @static - */ - DEFAULT : "yui-default" - }; - - /** - * Object literal containing the default configuration values for the CalendarNavigator - * The configuration object is expected to follow the format below, with the properties being - * case sensitive. - * <dl> - * <dt>strings</dt> - * <dd><em>Object</em> : An object with the properties shown below, defining the string labels to use in the Navigator's UI - * <dl> - * <dt>month</dt><dd><em>String</em> : The string to use for the month label. Defaults to "Month".</dd> - * <dt>year</dt><dd><em>String</em> : The string to use for the year label. Defaults to "Year".</dd> - * <dt>submit</dt><dd><em>String</em> : The string to use for the submit button label. Defaults to "Okay".</dd> - * <dt>cancel</dt><dd><em>String</em> : The string to use for the cancel button label. Defaults to "Cancel".</dd> - * <dt>invalidYear</dt><dd><em>String</em> : The string to use for invalid year values. Defaults to "Year needs to be a number".</dd> - * </dl> - * </dd> - * <dt>monthFormat</dt><dd><em>String</em> : The month format to use. Either YAHOO.widget.Calendar.LONG, or YAHOO.widget.Calendar.SHORT. Defaults to YAHOO.widget.Calendar.LONG</dd> - * <dt>initialFocus</dt><dd><em>String</em> : Either "year" or "month" specifying which input control should get initial focus. Defaults to "year"</dd> - * </dl> - * @property _DEFAULT_CFG - * @protected - * @type Object - * @static - */ - CN._DEFAULT_CFG = { - strings : { - month: "Month", - year: "Year", - submit: "Okay", - cancel: "Cancel", - invalidYear : "Year needs to be a number" - }, - monthFormat: YAHOO.widget.Calendar.LONG, - initialFocus: "year" - }; - - /** - * The suffix added to the Calendar/CalendarGroup's ID, to generate - * a unique ID for the Navigator and it's bounding box. - * @property YAHOO.widget.CalendarNavigator.ID_SUFFIX - * @static - * @type String - * @final - */ - CN.ID_SUFFIX = "_nav"; - /** - * The suffix added to the Navigator's ID, to generate - * a unique ID for the month control. - * @property YAHOO.widget.CalendarNavigator.MONTH_SUFFIX - * @static - * @type String - * @final - */ - CN.MONTH_SUFFIX = "_month"; - /** - * The suffix added to the Navigator's ID, to generate - * a unique ID for the year control. - * @property YAHOO.widget.CalendarNavigator.YEAR_SUFFIX - * @static - * @type String - * @final - */ - CN.YEAR_SUFFIX = "_year"; - /** - * The suffix added to the Navigator's ID, to generate - * a unique ID for the error bounding box. - * @property YAHOO.widget.CalendarNavigator.ERROR_SUFFIX - * @static - * @type String - * @final - */ - CN.ERROR_SUFFIX = "_error"; - /** - * The suffix added to the Navigator's ID, to generate - * a unique ID for the "Cancel" button. - * @property YAHOO.widget.CalendarNavigator.CANCEL_SUFFIX - * @static - * @type String - * @final - */ - CN.CANCEL_SUFFIX = "_cancel"; - /** - * The suffix added to the Navigator's ID, to generate - * a unique ID for the "Submit" button. - * @property YAHOO.widget.CalendarNavigator.SUBMIT_SUFFIX - * @static - * @type String - * @final - */ - CN.SUBMIT_SUFFIX = "_submit"; - - /** - * The number of digits to which the year input control is to be limited. - * @property YAHOO.widget.CalendarNavigator.YR_MAX_DIGITS - * @static - * @type Number - */ - CN.YR_MAX_DIGITS = 4; - - /** - * The amount by which to increment the current year value, - * when the arrow up/down key is pressed on the year control - * @property YAHOO.widget.CalendarNavigator.YR_MINOR_INC - * @static - * @type Number - */ - CN.YR_MINOR_INC = 1; - - /** - * The amount by which to increment the current year value, - * when the page up/down key is pressed on the year control - * @property YAHOO.widget.CalendarNavigator.YR_MAJOR_INC - * @static - * @type Number - */ - CN.YR_MAJOR_INC = 10; - - /** - * Artificial delay (in ms) between the time the Navigator is hidden - * and the Calendar/CalendarGroup state is updated. Allows the user - * the see the Calendar/CalendarGroup page changing. If set to 0 - * the Calendar/CalendarGroup page will be updated instantly - * @property YAHOO.widget.CalendarNavigator.UPDATE_DELAY - * @static - * @type Number - */ - CN.UPDATE_DELAY = 50; - - /** - * Regular expression used to validate the year input - * @property YAHOO.widget.CalendarNavigator.YR_PATTERN - * @static - * @type RegExp - */ - CN.YR_PATTERN = /^\d+$/; - /** - * Regular expression used to trim strings - * @property YAHOO.widget.CalendarNavigator.TRIM - * @static - * @type RegExp - */ - CN.TRIM = /^\s*(.*?)\s*$/; -})(); - -YAHOO.widget.CalendarNavigator.prototype = { - - /** - * The unique ID for this CalendarNavigator instance - * @property id - * @type String - */ - id : null, - - /** - * The Calendar/CalendarGroup instance to which the navigator belongs - * @property cal - * @type {Calendar|CalendarGroup} - */ - cal : null, - - /** - * Reference to the HTMLElement used to render the navigator's bounding box - * @property navEl - * @type HTMLElement - */ - navEl : null, - - /** - * Reference to the HTMLElement used to render the navigator's mask - * @property maskEl - * @type HTMLElement - */ - maskEl : null, - - /** - * Reference to the HTMLElement used to input the year - * @property yearEl - * @type HTMLElement - */ - yearEl : null, - - /** - * Reference to the HTMLElement used to input the month - * @property monthEl - * @type HTMLElement - */ - monthEl : null, - - /** - * Reference to the HTMLElement used to display validation errors - * @property errorEl - * @type HTMLElement - */ - errorEl : null, - - /** - * Reference to the HTMLElement used to update the Calendar/Calendar group - * with the month/year values - * @property submitEl - * @type HTMLElement - */ - submitEl : null, - - /** - * Reference to the HTMLElement used to hide the navigator without updating the - * Calendar/Calendar group - * @property cancelEl - * @type HTMLElement - */ - cancelEl : null, - - /** - * Reference to the first focusable control in the navigator (by default monthEl) - * @property firstCtrl - * @type HTMLElement - */ - firstCtrl : null, - - /** - * Reference to the last focusable control in the navigator (by default cancelEl) - * @property lastCtrl - * @type HTMLElement - */ - lastCtrl : null, - - /** - * The document containing the Calendar/Calendar group instance - * @protected - * @property _doc - * @type HTMLDocument - */ - _doc : null, - - /** - * Internal state property for the current year displayed in the navigator - * @protected - * @property _year - * @type Number - */ - _year: null, - - /** - * Internal state property for the current month index displayed in the navigator - * @protected - * @property _month - * @type Number - */ - _month: 0, - - /** - * Private internal state property which indicates whether or not the - * Navigator has been rendered. - * @private - * @property __rendered - * @type Boolean - */ - __rendered: false, - - /** - * Init lifecycle method called as part of construction - * - * @method init - * @param {Calendar} cal The instance of the Calendar or CalendarGroup to which this CalendarNavigator should be attached - */ - init : function(cal) { - var calBox = cal.oDomContainer; - - this.cal = cal; - this.id = calBox.id + YAHOO.widget.CalendarNavigator.ID_SUFFIX; - this._doc = calBox.ownerDocument; - - /** - * Private flag, to identify IE6/IE7 Quirks - * @private - * @property __isIEQuirks - */ - var ie = YAHOO.env.ua.ie; - this.__isIEQuirks = (ie && ((ie <= 6) || (ie === 7 && this._doc.compatMode == "BackCompat"))); - }, - - /** - * Displays the navigator and mask, updating the input controls to reflect the - * currently set month and year. The show method will invoke the render method - * if the navigator has not been renderered already, allowing for lazy rendering - * of the control. - * - * The show method will fire the Calendar/CalendarGroup's beforeShowNav and showNav events - * - * @method show - */ - show : function() { - var CLASSES = YAHOO.widget.CalendarNavigator.CLASSES; - - if (this.cal.beforeShowNavEvent.fire()) { - if (!this.__rendered) { - this.render(); - } - this.clearErrors(); - - this._updateMonthUI(); - this._updateYearUI(); - this._show(this.navEl, true); - - this.setInitialFocus(); - this.showMask(); - - YAHOO.util.Dom.addClass(this.cal.oDomContainer, CLASSES.NAV_VISIBLE); - this.cal.showNavEvent.fire(); - } - }, - - /** - * Hides the navigator and mask - * - * The show method will fire the Calendar/CalendarGroup's beforeHideNav event and hideNav events - * @method hide - */ - hide : function() { - var CLASSES = YAHOO.widget.CalendarNavigator.CLASSES; - - if (this.cal.beforeHideNavEvent.fire()) { - this._show(this.navEl, false); - this.hideMask(); - YAHOO.util.Dom.removeClass(this.cal.oDomContainer, CLASSES.NAV_VISIBLE); - this.cal.hideNavEvent.fire(); - } - }, - - - /** - * Displays the navigator's mask element - * - * @method showMask - */ - showMask : function() { - this._show(this.maskEl, true); - if (this.__isIEQuirks) { - this._syncMask(); - } - }, - - /** - * Hides the navigator's mask element - * - * @method hideMask - */ - hideMask : function() { - this._show(this.maskEl, false); - }, - - /** - * Returns the current month set on the navigator - * - * Note: This may not be the month set in the UI, if - * the UI contains an invalid value. - * - * @method getMonth - * @return {Number} The Navigator's current month index - */ - getMonth: function() { - return this._month; - }, - - /** - * Returns the current year set on the navigator - * - * Note: This may not be the year set in the UI, if - * the UI contains an invalid value. - * - * @method getYear - * @return {Number} The Navigator's current year value - */ - getYear: function() { - return this._year; - }, - - /** - * Sets the current month on the Navigator, and updates the UI - * - * @method setMonth - * @param {Number} nMonth The month index, from 0 (Jan) through 11 (Dec). - */ - setMonth : function(nMonth) { - if (nMonth >= 0 && nMonth < 12) { - this._month = nMonth; - } - this._updateMonthUI(); - }, - - /** - * Sets the current year on the Navigator, and updates the UI. If the - * provided year is invalid, it will not be set. - * - * @method setYear - * @param {Number} nYear The full year value to set the Navigator to. - */ - setYear : function(nYear) { - var yrPattern = YAHOO.widget.CalendarNavigator.YR_PATTERN; - if (YAHOO.lang.isNumber(nYear) && yrPattern.test(nYear+"")) { - this._year = nYear; - } - this._updateYearUI(); - }, - - /** - * Renders the HTML for the navigator, adding it to the - * document and attaches event listeners if it has not - * already been rendered. - * - * @method render - */ - render: function() { - this.cal.beforeRenderNavEvent.fire(); - if (!this.__rendered) { - this.createNav(); - this.createMask(); - this.applyListeners(); - this.__rendered = true; - } - this.cal.renderNavEvent.fire(); - }, - - /** - * Creates the navigator's containing HTMLElement, it's contents, and appends - * the containg element to the Calendar/CalendarGroup's container. - * - * @method createNav - */ - createNav : function() { - var NAV = YAHOO.widget.CalendarNavigator; - var doc = this._doc; - - var d = doc.createElement("div"); - d.className = NAV.CLASSES.NAV; - - var htmlBuf = this.renderNavContents([]); - - d.innerHTML = htmlBuf.join(''); - this.cal.oDomContainer.appendChild(d); - - this.navEl = d; - - this.yearEl = doc.getElementById(this.id + NAV.YEAR_SUFFIX); - this.monthEl = doc.getElementById(this.id + NAV.MONTH_SUFFIX); - this.errorEl = doc.getElementById(this.id + NAV.ERROR_SUFFIX); - this.submitEl = doc.getElementById(this.id + NAV.SUBMIT_SUFFIX); - this.cancelEl = doc.getElementById(this.id + NAV.CANCEL_SUFFIX); - - if (YAHOO.env.ua.gecko && this.yearEl && this.yearEl.type == "text") { - // Avoid XUL error on focus, select [ https://bugzilla.mozilla.org/show_bug.cgi?id=236791, - // supposedly fixed in 1.8.1, but there are reports of it still being around for methods other than blur ] - this.yearEl.setAttribute("autocomplete", "off"); - } - - this._setFirstLastElements(); - }, - - /** - * Creates the Mask HTMLElement and appends it to the Calendar/CalendarGroups - * container. - * - * @method createMask - */ - createMask : function() { - var C = YAHOO.widget.CalendarNavigator.CLASSES; - - var d = this._doc.createElement("div"); - d.className = C.MASK; - - this.cal.oDomContainer.appendChild(d); - this.maskEl = d; - }, - - /** - * Used to set the width/height of the mask in pixels to match the Calendar Container. - * Currently only used for IE6 and IE7 quirks mode. The other A-Grade browser are handled using CSS (width/height 100%). - * <p> - * The method is also registered as an HTMLElement resize listener on the Calendars container element. - * </p> - * @protected - * @method _syncMask - */ - _syncMask : function() { - var c = this.cal.oDomContainer; - if (c && this.maskEl) { - var r = YAHOO.util.Dom.getRegion(c); - YAHOO.util.Dom.setStyle(this.maskEl, "width", r.right - r.left + "px"); - YAHOO.util.Dom.setStyle(this.maskEl, "height", r.bottom - r.top + "px"); - } - }, - - /** - * Renders the contents of the navigator - * - * @method renderNavContents - * - * @param {Array} html The HTML buffer to append the HTML to. - * @return {Array} A reference to the buffer passed in. - */ - renderNavContents : function(html) { - var NAV = YAHOO.widget.CalendarNavigator, - C = NAV.CLASSES, - h = html; // just to use a shorter name - - h[h.length] = '<div class="' + C.MONTH + '">'; - this.renderMonth(h); - h[h.length] = '</div>'; - h[h.length] = '<div class="' + C.YEAR + '">'; - this.renderYear(h); - h[h.length] = '</div>'; - h[h.length] = '<div class="' + C.BUTTONS + '">'; - this.renderButtons(h); - h[h.length] = '</div>'; - h[h.length] = '<div class="' + C.ERROR + '" id="' + this.id + NAV.ERROR_SUFFIX + '"></div>'; - - return h; - }, - - /** - * Renders the month label and control for the navigator - * - * @method renderNavContents - * @param {Array} html The HTML buffer to append the HTML to. - * @return {Array} A reference to the buffer passed in. - */ - renderMonth : function(html) { - var NAV = YAHOO.widget.CalendarNavigator, - C = NAV.CLASSES; - - var id = this.id + NAV.MONTH_SUFFIX, - mf = this.__getCfg("monthFormat"), - months = this.cal.cfg.getProperty((mf == YAHOO.widget.Calendar.SHORT) ? "MONTHS_SHORT" : "MONTHS_LONG"), - h = html; - - if (months && months.length > 0) { - h[h.length] = '<label for="' + id + '">'; - h[h.length] = this.__getCfg("month", true); - h[h.length] = '</label>'; - h[h.length] = '<select name="' + id + '" id="' + id + '" class="' + C.MONTH_CTRL + '">'; - for (var i = 0; i < months.length; i++) { - h[h.length] = '<option value="' + i + '">'; - h[h.length] = months[i]; - h[h.length] = '</option>'; - } - h[h.length] = '</select>'; - } - return h; - }, - - /** - * Renders the year label and control for the navigator - * - * @method renderYear - * @param {Array} html The HTML buffer to append the HTML to. - * @return {Array} A reference to the buffer passed in. - */ - renderYear : function(html) { - var NAV = YAHOO.widget.CalendarNavigator, - C = NAV.CLASSES; - - var id = this.id + NAV.YEAR_SUFFIX, - size = NAV.YR_MAX_DIGITS, - h = html; - - h[h.length] = '<label for="' + id + '">'; - h[h.length] = this.__getCfg("year", true); - h[h.length] = '</label>'; - h[h.length] = '<input type="text" name="' + id + '" id="' + id + '" class="' + C.YEAR_CTRL + '" maxlength="' + size + '"/>'; - return h; - }, - - /** - * Renders the submit/cancel buttons for the navigator - * - * @method renderButton - * @return {String} The HTML created for the Button UI - */ - renderButtons : function(html) { - var C = YAHOO.widget.CalendarNavigator.CLASSES; - var h = html; - - h[h.length] = '<span class="' + C.BUTTON + ' ' + C.DEFAULT + '">'; - h[h.length] = '<button type="button" id="' + this.id + '_submit' + '">'; - h[h.length] = this.__getCfg("submit", true); - h[h.length] = '</button>'; - h[h.length] = '</span>'; - h[h.length] = '<span class="' + C.BUTTON +'">'; - h[h.length] = '<button type="button" id="' + this.id + '_cancel' + '">'; - h[h.length] = this.__getCfg("cancel", true); - h[h.length] = '</button>'; - h[h.length] = '</span>'; - - return h; - }, - - /** - * Attaches DOM event listeners to the rendered elements - * <p> - * The method will call applyKeyListeners, to setup keyboard specific - * listeners - * </p> - * @method applyListeners - */ - applyListeners : function() { - var E = YAHOO.util.Event; - - function yearUpdateHandler() { - if (this.validate()) { - this.setYear(this._getYearFromUI()); - } - } - - function monthUpdateHandler() { - this.setMonth(this._getMonthFromUI()); - } - - E.on(this.submitEl, "click", this.submit, this, true); - E.on(this.cancelEl, "click", this.cancel, this, true); - E.on(this.yearEl, "blur", yearUpdateHandler, this, true); - E.on(this.monthEl, "change", monthUpdateHandler, this, true); - - if (this.__isIEQuirks) { - YAHOO.util.Event.on(this.cal.oDomContainer, "resize", this._syncMask, this, true); - } - - this.applyKeyListeners(); - }, - - /** - * Removes/purges DOM event listeners from the rendered elements - * - * @method purgeListeners - */ - purgeListeners : function() { - var E = YAHOO.util.Event; - E.removeListener(this.submitEl, "click", this.submit); - E.removeListener(this.cancelEl, "click", this.cancel); - E.removeListener(this.yearEl, "blur"); - E.removeListener(this.monthEl, "change"); - if (this.__isIEQuirks) { - E.removeListener(this.cal.oDomContainer, "resize", this._syncMask); - } - - this.purgeKeyListeners(); - }, - - /** - * Attaches DOM listeners for keyboard support. - * Tab/Shift-Tab looping, Enter Key Submit on Year element, - * Up/Down/PgUp/PgDown year increment on Year element - * <p> - * NOTE: MacOSX Safari 2.x doesn't let you tab to buttons and - * MacOSX Gecko does not let you tab to buttons or select controls, - * so for these browsers, Tab/Shift-Tab looping is limited to the - * elements which can be reached using the tab key. - * </p> - * @method applyKeyListeners - */ - applyKeyListeners : function() { - var E = YAHOO.util.Event; - - // IE doesn't fire keypress for arrow/pg keys (non-char keys) - var ua = YAHOO.env.ua; - var arrowEvt = (ua.ie) ? "keydown" : "keypress"; - - // - IE doesn't fire keypress for non-char keys - // - Opera doesn't allow us to cancel keydown or keypress for tab, but - // changes focus successfully on keydown (keypress is too late to change focus - opera's already moved on). - var tabEvt = (ua.ie || ua.opera) ? "keydown" : "keypress"; - - // Everyone likes keypress for Enter (char keys) - whoo hoo! - E.on(this.yearEl, "keypress", this._handleEnterKey, this, true); - - E.on(this.yearEl, arrowEvt, this._handleDirectionKeys, this, true); - E.on(this.lastCtrl, tabEvt, this._handleTabKey, this, true); - E.on(this.firstCtrl, tabEvt, this._handleShiftTabKey, this, true); - }, - - /** - * Removes/purges DOM listeners for keyboard support - * - * @method purgeKeyListeners - */ - purgeKeyListeners : function() { - var E = YAHOO.util.Event; - - var arrowEvt = (YAHOO.env.ua.ie) ? "keydown" : "keypress"; - var tabEvt = (YAHOO.env.ua.ie || YAHOO.env.ua.opera) ? "keydown" : "keypress"; - - E.removeListener(this.yearEl, "keypress", this._handleEnterKey); - E.removeListener(this.yearEl, arrowEvt, this._handleDirectionKeys); - E.removeListener(this.lastCtrl, tabEvt, this._handleTabKey); - E.removeListener(this.firstCtrl, tabEvt, this._handleShiftTabKey); - }, - - /** - * Updates the Calendar/CalendarGroup's pagedate with the currently set month and year if valid. - * <p> - * If the currently set month/year is invalid, a validation error will be displayed and the - * Calendar/CalendarGroup's pagedate will not be updated. - * </p> - * @method submit - */ - submit : function() { - if (this.validate()) { - this.hide(); - - this.setMonth(this._getMonthFromUI()); - this.setYear(this._getYearFromUI()); - - var cal = this.cal; - var nav = this; - - function update() { - cal.setYear(nav.getYear()); - cal.setMonth(nav.getMonth()); - cal.render(); - } - // Artificial delay, just to help the user see something changed - var delay = YAHOO.widget.CalendarNavigator.UPDATE_DELAY; - if (delay > 0) { - window.setTimeout(update, delay); - } else { - update(); - } - } - }, - - /** - * Hides the navigator and mask, without updating the Calendar/CalendarGroup's state - * - * @method cancel - */ - cancel : function() { - this.hide(); - }, - - /** - * Validates the current state of the UI controls - * - * @method validate - * @return {Boolean} true, if the current UI state contains valid values, false if not - */ - validate : function() { - if (this._getYearFromUI() !== null) { - this.clearErrors(); - return true; - } else { - this.setYearError(); - this.setError(this.__getCfg("invalidYear", true)); - return false; - } - }, - - /** - * Displays an error message in the Navigator's error panel - * @method setError - * @param {String} msg The error message to display - */ - setError : function(msg) { - if (this.errorEl) { - this.errorEl.innerHTML = msg; - this._show(this.errorEl, true); - } - }, - - /** - * Clears the navigator's error message and hides the error panel - * @method clearError - */ - clearError : function() { - if (this.errorEl) { - this.errorEl.innerHTML = ""; - this._show(this.errorEl, false); - } - }, - - /** - * Displays the validation error UI for the year control - * @method setYearError - */ - setYearError : function() { - YAHOO.util.Dom.addClass(this.yearEl, YAHOO.widget.CalendarNavigator.CLASSES.INVALID); - }, - - /** - * Removes the validation error UI for the year control - * @method clearYearError - */ - clearYearError : function() { - YAHOO.util.Dom.removeClass(this.yearEl, YAHOO.widget.CalendarNavigator.CLASSES.INVALID); - }, - - /** - * Clears all validation and error messages in the UI - * @method clearErrors - */ - clearErrors : function() { - this.clearError(); - this.clearYearError(); - }, - - /** - * Sets the initial focus, based on the configured value - * @method setInitialFocus - */ - setInitialFocus : function() { - var el = this.submitEl; - var f = this.__getCfg("initialFocus"); - - if (f && f.toLowerCase) { - f = f.toLowerCase(); - if (f == "year") { - el = this.yearEl; - try { - this.yearEl.select(); - } catch (e) { - // Ignore; - } - } else if (f == "month") { - el = this.monthEl; - } - } - - if (el && YAHOO.lang.isFunction(el.focus)) { - try { - el.focus(); - } catch (e) { - // TODO: Fall back if focus fails? - } - } - }, - - /** - * Removes all renderered HTML elements for the Navigator from - * the DOM, purges event listeners and clears (nulls) any property - * references to HTML references - * @method erase - */ - erase : function() { - if (this.__rendered) { - this.purgeListeners(); - - // Clear out innerHTML references - this.yearEl = null; - this.monthEl = null; - this.errorEl = null; - this.submitEl = null; - this.cancelEl = null; - this.firstCtrl = null; - this.lastCtrl = null; - if (this.navEl) { - this.navEl.innerHTML = ""; - } - - var p = this.navEl.parentNode; - if (p) { - p.removeChild(this.navEl); - } - this.navEl = null; - - var pm = this.maskEl.parentNode; - if (pm) { - pm.removeChild(this.maskEl); - } - this.maskEl = null; - this.__rendered = false; - } - }, - - /** - * Destroys the Navigator object and any HTML references - * @method destroy - */ - destroy : function() { - this.erase(); - this._doc = null; - this.cal = null; - this.id = null; - }, - - /** - * Protected implementation to handle how UI elements are - * hidden/shown. - * - * @method _show - * @protected - */ - _show : function(el, bShow) { - if (el) { - YAHOO.util.Dom.setStyle(el, "display", (bShow) ? "block" : "none"); - } - }, - - /** - * Returns the month value (index), from the month UI element - * @protected - * @method _getMonthFromUI - * @return {Number} The month index, or 0 if a UI element for the month - * is not found - */ - _getMonthFromUI : function() { - if (this.monthEl) { - return this.monthEl.selectedIndex; - } else { - return 0; // Default to Jan - } - }, - - /** - * Returns the year value, from the Navitator's year UI element - * @protected - * @method _getYearFromUI - * @return {Number} The year value set in the UI, if valid. null is returned if - * the UI does not contain a valid year value. - */ - _getYearFromUI : function() { - var NAV = YAHOO.widget.CalendarNavigator; - - var yr = null; - if (this.yearEl) { - var value = this.yearEl.value; - value = value.replace(NAV.TRIM, "$1"); - - if (NAV.YR_PATTERN.test(value)) { - yr = parseInt(value, 10); - } - } - return yr; - }, - - /** - * Updates the Navigator's year UI, based on the year value set on the Navigator object - * @protected - * @method _updateYearUI - */ - _updateYearUI : function() { - if (this.yearEl && this._year !== null) { - this.yearEl.value = this._year; - } - }, - - /** - * Updates the Navigator's month UI, based on the month value set on the Navigator object - * @protected - * @method _updateMonthUI - */ - _updateMonthUI : function() { - if (this.monthEl) { - this.monthEl.selectedIndex = this._month; - } - }, - - /** - * Sets up references to the first and last focusable element in the Navigator's UI - * in terms of tab order (Naviagator's firstEl and lastEl properties). The references - * are used to control modality by looping around from the first to the last control - * and visa versa for tab/shift-tab navigation. - * <p> - * See <a href="#applyKeyListeners">applyKeyListeners</a> - * </p> - * @protected - * @method _setFirstLastElements - */ - _setFirstLastElements : function() { - this.firstCtrl = this.monthEl; - this.lastCtrl = this.cancelEl; - - // Special handling for MacOSX. - // - Safari 2.x can't focus on buttons - // - Gecko can't focus on select boxes or buttons - if (this.__isMac) { - if (YAHOO.env.ua.webkit && YAHOO.env.ua.webkit < 420){ - this.firstCtrl = this.monthEl; - this.lastCtrl = this.yearEl; - } - if (YAHOO.env.ua.gecko) { - this.firstCtrl = this.yearEl; - this.lastCtrl = this.yearEl; - } - } - }, - - /** - * Default Keyboard event handler to capture Enter - * on the Navigator's year control (yearEl) - * - * @method _handleEnterKey - * @protected - * @param {Event} e The DOM event being handled - */ - _handleEnterKey : function(e) { - var KEYS = YAHOO.util.KeyListener.KEY; - - if (YAHOO.util.Event.getCharCode(e) == KEYS.ENTER) { - this.submit(); - } - }, - - /** - * Default Keyboard event handler to capture up/down/pgup/pgdown - * on the Navigator's year control (yearEl). - * - * @method _handleDirectionKeys - * @protected - * @param {Event} e The DOM event being handled - */ - _handleDirectionKeys : function(e) { - var E = YAHOO.util.Event; - var KEYS = YAHOO.util.KeyListener.KEY; - var NAV = YAHOO.widget.CalendarNavigator; - - var value = (this.yearEl.value) ? parseInt(this.yearEl.value, 10) : null; - if (isFinite(value)) { - var dir = false; - switch(E.getCharCode(e)) { - case KEYS.UP: - this.yearEl.value = value + NAV.YR_MINOR_INC; - dir = true; - break; - case KEYS.DOWN: - this.yearEl.value = Math.max(value - NAV.YR_MINOR_INC, 0); - dir = true; - break; - case KEYS.PAGE_UP: - this.yearEl.value = value + NAV.YR_MAJOR_INC; - dir = true; - break; - case KEYS.PAGE_DOWN: - this.yearEl.value = Math.max(value - NAV.YR_MAJOR_INC, 0); - dir = true; - break; - default: - break; - } - if (dir) { - E.preventDefault(e); - try { - this.yearEl.select(); - } catch(e) { - // Ignore - } - } - } - }, - - /** - * Default Keyboard event handler to capture Tab - * on the last control (lastCtrl) in the Navigator. - * - * @method _handleTabKey - * @protected - * @param {Event} e The DOM event being handled - */ - _handleTabKey : function(e) { - var E = YAHOO.util.Event; - var KEYS = YAHOO.util.KeyListener.KEY; - - if (E.getCharCode(e) == KEYS.TAB && !e.shiftKey) { - try { - E.preventDefault(e); - this.firstCtrl.focus(); - } catch (e) { - // Ignore - mainly for focus edge cases - } - } - }, - - /** - * Default Keyboard event handler to capture Shift-Tab - * on the first control (firstCtrl) in the Navigator. - * - * @method _handleShiftTabKey - * @protected - * @param {Event} e The DOM event being handled - */ - _handleShiftTabKey : function(e) { - var E = YAHOO.util.Event; - var KEYS = YAHOO.util.KeyListener.KEY; - - if (e.shiftKey && E.getCharCode(e) == KEYS.TAB) { - try { - E.preventDefault(e); - this.lastCtrl.focus(); - } catch (e) { - // Ignore - mainly for focus edge cases - } - } - }, - - /** - * Retrieve Navigator configuration values from - * the parent Calendar/CalendarGroup's config value. - * <p> - * If it has not been set in the user provided configuration, the method will - * return the default value of the configuration property, as set in _DEFAULT_CFG - * </p> - * @private - * @method __getCfg - * @param {String} Case sensitive property name. - * @param {Boolean} true, if the property is a string property, false if not. - * @return The value of the configuration property - */ - __getCfg : function(prop, bIsStr) { - var DEF_CFG = YAHOO.widget.CalendarNavigator._DEFAULT_CFG; - var cfg = this.cal.cfg.getProperty("navigator"); - - if (bIsStr) { - return (cfg !== true && cfg.strings && cfg.strings[prop]) ? cfg.strings[prop] : DEF_CFG.strings[prop]; - } else { - return (cfg !== true && cfg[prop]) ? cfg[prop] : DEF_CFG[prop]; - } - }, - - /** - * Private flag, to identify MacOS - * @private - * @property __isMac - */ - __isMac : (navigator.userAgent.toLowerCase().indexOf("macintosh") != -1) - -}; - -YAHOO.register("calendar", YAHOO.widget.Calendar, {version: "2.4.1", build: "742"}); diff --git a/sonar-server/src/main/webapp/javascripts/calendar/yahoo-dom-event.js b/sonar-server/src/main/webapp/javascripts/calendar/yahoo-dom-event-min.js index 106a14472c4..106a14472c4 100644 --- a/sonar-server/src/main/webapp/javascripts/calendar/yahoo-dom-event.js +++ b/sonar-server/src/main/webapp/javascripts/calendar/yahoo-dom-event-min.js diff --git a/sonar-server/src/main/webapp/javascripts/prototype.js b/sonar-server/src/main/webapp/javascripts/prototype.js index 845ab7fb4eb..04a4779398a 100644 --- a/sonar-server/src/main/webapp/javascripts/prototype.js +++ b/sonar-server/src/main/webapp/javascripts/prototype.js @@ -1,5 +1,5 @@ -/* Prototype JavaScript framework, version 1.6.1 - * (c) 2005-2009 Sam Stephenson +/* Prototype JavaScript framework, version 1.7 + * (c) 2005-2010 Sam Stephenson * * Prototype is freely distributable under the terms of an MIT-style license. * For details, see the Prototype web site: http://www.prototypejs.org/ @@ -7,7 +7,8 @@ *--------------------------------------------------------------------------*/ var Prototype = { - Version: '1.6.1', + + Version: '1.7', Browser: (function(){ var ua = navigator.userAgent; @@ -17,13 +18,15 @@ var Prototype = { Opera: isOpera, WebKit: ua.indexOf('AppleWebKit/') > -1, Gecko: ua.indexOf('Gecko') > -1 && ua.indexOf('KHTML') === -1, - MobileSafari: /Apple.*Mobile.*Safari/.test(ua) + MobileSafari: /Apple.*Mobile/.test(ua) } })(), BrowserFeatures: { XPath: !!document.evaluate, + SelectorsAPI: !!document.querySelector, + ElementExtensions: (function() { var constructor = window.Element || window.HTMLElement; return !!(constructor && constructor.prototype); @@ -32,9 +35,9 @@ var Prototype = { if (typeof window.HTMLDivElement !== 'undefined') return true; - var div = document.createElement('div'); - var form = document.createElement('form'); - var isSupported = false; + var div = document.createElement('div'), + form = document.createElement('form'), + isSupported = false; if (div['__proto__'] && (div['__proto__'] !== form['__proto__'])) { isSupported = true; @@ -50,35 +53,23 @@ var Prototype = { JSONFilter: /^\/\*-secure-([\s\S]*)\*\/\s*$/, emptyFunction: function() { }, + K: function(x) { return x } }; if (Prototype.Browser.MobileSafari) Prototype.BrowserFeatures.SpecificElementExtensions = false; +/* Based on Alex Arnell's inheritance implementation. */ +var Class = (function() { -var Abstract = { }; - - -var Try = { - these: function() { - var returnValue; - - for (var i = 0, length = arguments.length; i < length; i++) { - var lambda = arguments[i]; - try { - returnValue = lambda(); - break; - } catch (e) { } + var IS_DONTENUM_BUGGY = (function(){ + for (var p in { toString: 1 }) { + if (p === 'toString') return false; } + return true; + })(); - return returnValue; - } -}; - -/* Based on Alex Arnell's inheritance implementation. */ - -var Class = (function() { function subclass() {}; function create() { var parent = null, properties = $A(arguments); @@ -99,7 +90,7 @@ var Class = (function() { parent.subclasses.push(klass); } - for (var i = 0; i < properties.length; i++) + for (var i = 0, length = properties.length; i < length; i++) klass.addMethods(properties[i]); if (!klass.prototype.initialize) @@ -110,10 +101,10 @@ var Class = (function() { } function addMethods(source) { - var ancestor = this.superclass && this.superclass.prototype; - var properties = Object.keys(source); + var ancestor = this.superclass && this.superclass.prototype, + properties = Object.keys(source); - if (!Object.keys({ toString: true }).length) { + if (IS_DONTENUM_BUGGY) { if (source.toString != Object.prototype.toString) properties.push("toString"); if (source.valueOf != Object.prototype.valueOf) @@ -123,7 +114,7 @@ var Class = (function() { for (var i = 0, length = properties.length; i < length; i++) { var property = properties[i], value = source[property]; if (ancestor && Object.isFunction(value) && - value.argumentNames().first() == "$super") { + value.argumentNames()[0] == "$super") { var method = value; value = (function(m) { return function() { return ancestor[m].apply(this, arguments); }; @@ -147,7 +138,37 @@ var Class = (function() { })(); (function() { - var _toString = Object.prototype.toString; + var _toString = Object.prototype.toString, + NULL_TYPE = 'Null', + UNDEFINED_TYPE = 'Undefined', + BOOLEAN_TYPE = 'Boolean', + NUMBER_TYPE = 'Number', + STRING_TYPE = 'String', + OBJECT_TYPE = 'Object', + FUNCTION_CLASS = '[object Function]', + BOOLEAN_CLASS = '[object Boolean]', + NUMBER_CLASS = '[object Number]', + STRING_CLASS = '[object String]', + ARRAY_CLASS = '[object Array]', + DATE_CLASS = '[object Date]', + NATIVE_JSON_STRINGIFY_SUPPORT = window.JSON && + typeof JSON.stringify === 'function' && + JSON.stringify(0) === '0' && + typeof JSON.stringify(Prototype.K) === 'undefined'; + + function Type(o) { + switch(o) { + case null: return NULL_TYPE; + case (void 0): return UNDEFINED_TYPE; + } + var type = typeof o; + switch(type) { + case 'boolean': return BOOLEAN_TYPE; + case 'number': return NUMBER_TYPE; + case 'string': return STRING_TYPE; + } + return OBJECT_TYPE; + } function extend(destination, source) { for (var property in source) @@ -166,27 +187,70 @@ var Class = (function() { } } - function toJSON(object) { - var type = typeof object; - switch (type) { - case 'undefined': - case 'function': - case 'unknown': return; - case 'boolean': return object.toString(); + function toJSON(value) { + return Str('', { '': value }, []); + } + + function Str(key, holder, stack) { + var value = holder[key], + type = typeof value; + + if (Type(value) === OBJECT_TYPE && typeof value.toJSON === 'function') { + value = value.toJSON(key); } - if (object === null) return 'null'; - if (object.toJSON) return object.toJSON(); - if (isElement(object)) return; + var _class = _toString.call(value); - var results = []; - for (var property in object) { - var value = toJSON(object[property]); - if (!isUndefined(value)) - results.push(property.toJSON() + ': ' + value); + switch (_class) { + case NUMBER_CLASS: + case BOOLEAN_CLASS: + case STRING_CLASS: + value = value.valueOf(); + } + + switch (value) { + case null: return 'null'; + case true: return 'true'; + case false: return 'false'; + } + + type = typeof value; + switch (type) { + case 'string': + return value.inspect(true); + case 'number': + return isFinite(value) ? String(value) : 'null'; + case 'object': + + for (var i = 0, length = stack.length; i < length; i++) { + if (stack[i] === value) { throw new TypeError(); } + } + stack.push(value); + + var partial = []; + if (_class === ARRAY_CLASS) { + for (var i = 0, length = value.length; i < length; i++) { + var str = Str(i, value, stack); + partial.push(typeof str === 'undefined' ? 'null' : str); + } + partial = '[' + partial.join(',') + ']'; + } else { + var keys = Object.keys(value); + for (var i = 0, length = keys.length; i < length; i++) { + var key = keys[i], str = Str(key, value, stack); + if (typeof str !== "undefined") { + partial.push(key.inspect(true)+ ':' + str); + } + } + partial = '{' + partial.join(',') + '}'; + } + stack.pop(); + return partial; } + } - return '{' + results.join(', ') + '}'; + function stringify(object) { + return JSON.stringify(object); } function toQueryString(object) { @@ -198,9 +262,13 @@ var Class = (function() { } function keys(object) { + if (Type(object) !== OBJECT_TYPE) { throw new TypeError(); } var results = []; - for (var property in object) - results.push(property); + for (var property in object) { + if (object.hasOwnProperty(property)) { + results.push(property); + } + } return results; } @@ -220,24 +288,34 @@ var Class = (function() { } function isArray(object) { - return _toString.call(object) == "[object Array]"; + return _toString.call(object) === ARRAY_CLASS; } + var hasNativeIsArray = (typeof Array.isArray == 'function') + && Array.isArray([]) && !Array.isArray({}); + + if (hasNativeIsArray) { + isArray = Array.isArray; + } function isHash(object) { return object instanceof Hash; } function isFunction(object) { - return typeof object === "function"; + return _toString.call(object) === FUNCTION_CLASS; } function isString(object) { - return _toString.call(object) == "[object String]"; + return _toString.call(object) === STRING_CLASS; } function isNumber(object) { - return _toString.call(object) == "[object Number]"; + return _toString.call(object) === NUMBER_CLASS; + } + + function isDate(object) { + return _toString.call(object) === DATE_CLASS; } function isUndefined(object) { @@ -247,10 +325,10 @@ var Class = (function() { extend(Object, { extend: extend, inspect: inspect, - toJSON: toJSON, + toJSON: NATIVE_JSON_STRINGIFY_SUPPORT ? stringify : toJSON, toQueryString: toQueryString, toHTML: toHTML, - keys: keys, + keys: Object.keys || keys, values: values, clone: clone, isElement: isElement, @@ -259,6 +337,7 @@ var Class = (function() { isFunction: isFunction, isString: isString, isNumber: isNumber, + isDate: isDate, isUndefined: isUndefined }); })(); @@ -311,7 +390,7 @@ Object.extend(Function.prototype, (function() { function delay(timeout) { var __method = this, args = slice.call(arguments, 1); - timeout = timeout * 1000 + timeout = timeout * 1000; return window.setTimeout(function() { return __method.apply(__method, args); }, timeout); @@ -352,14 +431,28 @@ Object.extend(Function.prototype, (function() { })()); -Date.prototype.toJSON = function() { - return '"' + this.getUTCFullYear() + '-' + - (this.getUTCMonth() + 1).toPaddedString(2) + '-' + - this.getUTCDate().toPaddedString(2) + 'T' + - this.getUTCHours().toPaddedString(2) + ':' + - this.getUTCMinutes().toPaddedString(2) + ':' + - this.getUTCSeconds().toPaddedString(2) + 'Z"'; -}; + +(function(proto) { + + + function toISOString() { + return this.getUTCFullYear() + '-' + + (this.getUTCMonth() + 1).toPaddedString(2) + '-' + + this.getUTCDate().toPaddedString(2) + 'T' + + this.getUTCHours().toPaddedString(2) + ':' + + this.getUTCMinutes().toPaddedString(2) + ':' + + this.getUTCSeconds().toPaddedString(2) + 'Z'; + } + + + function toJSON() { + return this.toISOString(); + } + + if (!proto.toISOString) proto.toISOString = toISOString; + if (!proto.toJSON) proto.toJSON = toJSON; + +})(Date.prototype); RegExp.prototype.match = RegExp.prototype.test; @@ -418,6 +511,9 @@ Object.extend(String, { }); Object.extend(String.prototype, (function() { + var NATIVE_JSON_PARSE_SUPPORT = window.JSON && + typeof JSON.parse === 'function' && + JSON.parse('{"test": true}').test; function prepareReplacement(replacement) { if (Object.isFunction(replacement)) return replacement; @@ -484,8 +580,8 @@ Object.extend(String.prototype, (function() { } function extractScripts() { - var matchAll = new RegExp(Prototype.ScriptFragment, 'img'); - var matchOne = new RegExp(Prototype.ScriptFragment, 'im'); + var matchAll = new RegExp(Prototype.ScriptFragment, 'img'), + matchOne = new RegExp(Prototype.ScriptFragment, 'im'); return (this.match(matchAll) || []).map(function(scriptTag) { return (scriptTag.match(matchOne) || ['', ''])[1]; }); @@ -510,8 +606,9 @@ Object.extend(String.prototype, (function() { return match[1].split(separator || '&').inject({ }, function(hash, pair) { if ((pair = pair.split('='))[0]) { - var key = decodeURIComponent(pair.shift()); - var value = pair.length > 1 ? pair.join('=') : pair[0]; + var key = decodeURIComponent(pair.shift()), + value = pair.length > 1 ? pair.join('=') : pair[0]; + if (value != undefined) value = decodeURIComponent(value); if (key in hash) { @@ -538,17 +635,9 @@ Object.extend(String.prototype, (function() { } function camelize() { - var parts = this.split('-'), len = parts.length; - if (len == 1) return parts[0]; - - var camelized = this.charAt(0) == '-' - ? parts[0].charAt(0).toUpperCase() + parts[0].substring(1) - : parts[0]; - - for (var i = 1; i < len; i++) - camelized += parts[i].charAt(0).toUpperCase() + parts[i].substring(1); - - return camelized; + return this.replace(/-+(.)?/g, function(match, chr) { + return chr ? chr.toUpperCase() : ''; + }); } function capitalize() { @@ -578,10 +667,6 @@ Object.extend(String.prototype, (function() { return "'" + escapedString.replace(/'/g, '\\\'') + "'"; } - function toJSON() { - return this.inspect(true); - } - function unfilterJSON(filter) { return this.replace(filter || Prototype.JSONFilter, '$1'); } @@ -589,29 +674,42 @@ Object.extend(String.prototype, (function() { function isJSON() { var str = this; if (str.blank()) return false; - str = this.replace(/\\./g, '@').replace(/"[^"\\\n\r]*"/g, ''); - return (/^[,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]*$/).test(str); + str = str.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@'); + str = str.replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']'); + str = str.replace(/(?:^|:|,)(?:\s*\[)+/g, ''); + return (/^[\],:{}\s]*$/).test(str); } function evalJSON(sanitize) { - var json = this.unfilterJSON(); + var json = this.unfilterJSON(), + cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g; + if (cx.test(json)) { + json = json.replace(cx, function (a) { + return '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); + }); + } try { if (!sanitize || json.isJSON()) return eval('(' + json + ')'); } catch (e) { } throw new SyntaxError('Badly formed JSON string: ' + this.inspect()); } + function parseJSON() { + var json = this.unfilterJSON(); + return JSON.parse(json); + } + function include(pattern) { return this.indexOf(pattern) > -1; } function startsWith(pattern) { - return this.indexOf(pattern) === 0; + return this.lastIndexOf(pattern, 0) === 0; } function endsWith(pattern) { var d = this.length - pattern.length; - return d >= 0 && this.lastIndexOf(pattern) === d; + return d >= 0 && this.indexOf(pattern, d) === d; } function empty() { @@ -631,7 +729,7 @@ Object.extend(String.prototype, (function() { sub: sub, scan: scan, truncate: truncate, - strip: String.prototype.trim ? String.prototype.trim : strip, + strip: String.prototype.trim || strip, stripTags: stripTags, stripScripts: stripScripts, extractScripts: extractScripts, @@ -648,10 +746,9 @@ Object.extend(String.prototype, (function() { underscore: underscore, dasherize: dasherize, inspect: inspect, - toJSON: toJSON, unfilterJSON: unfilterJSON, isJSON: isJSON, - evalJSON: evalJSON, + evalJSON: NATIVE_JSON_PARSE_SUPPORT ? parseJSON : evalJSON, include: include, startsWith: startsWith, endsWith: endsWith, @@ -677,8 +774,9 @@ var Template = Class.create({ var before = match[1] || ''; if (before == '\\') return match[2]; - var ctx = object, expr = match[3]; - var pattern = /^([^.[]+|\[((?:.*?[^\\])?)\])(\.|\[|$)/; + var ctx = object, expr = match[3], + pattern = /^([^.[]+|\[((?:.*?[^\\])?)\])(\.|\[|$)/; + match = pattern.exec(expr); if (match == null) return before; @@ -943,6 +1041,7 @@ var Enumerable = (function() { find: detect }; })(); + function $A(iterable) { if (!iterable) return []; if ('toArray' in Object(iterable)) return iterable.toArray(); @@ -951,6 +1050,7 @@ function $A(iterable) { return results; } + function $w(string) { if (!Object.isString(string)) return []; string = string.strip(); @@ -965,9 +1065,10 @@ Array.from = $A; slice = arrayProto.slice, _each = arrayProto.forEach; // use native browser JS 1.6 implementation if available - function each(iterator) { - for (var i = 0, length = this.length; i < length; i++) - iterator(this[i]); + function each(iterator, context) { + for (var i = 0, length = this.length >>> 0; i < length; i++) { + if (i in this) iterator.call(context, this[i], i, this); + } } if (!_each) _each = each; @@ -1007,7 +1108,7 @@ Array.from = $A; } function reverse(inline) { - return (inline !== false ? this : this.toArray())._reverse(); + return (inline === false ? this.toArray() : this)._reverse(); } function uniq(sorted) { @@ -1037,15 +1138,6 @@ Array.from = $A; return '[' + this.map(Object.inspect).join(', ') + ']'; } - function toJSON() { - var results = []; - this.each(function(object) { - var value = Object.toJSON(object); - if (!Object.isUndefined(value)) results.push(value); - }); - return '[' + results.join(', ') + ']'; - } - function indexOf(item, i) { i || (i = 0); var length = this.length; @@ -1094,8 +1186,7 @@ Array.from = $A; clone: clone, toArray: clone, size: size, - inspect: inspect, - toJSON: toJSON + inspect: inspect }); var CONCAT_ARGUMENTS_BUGGY = (function() { @@ -1116,6 +1207,7 @@ var Hash = Class.create(Enumerable, (function() { this._object = Object.isHash(object) ? object.toObject() : Object.clone(object); } + function _each(iterator) { for (var key in this._object) { var value = this._object[key], pair = [key, value]; @@ -1144,6 +1236,8 @@ var Hash = Class.create(Enumerable, (function() { return Object.clone(this._object); } + + function keys() { return this.pluck('key'); } @@ -1180,8 +1274,14 @@ var Hash = Class.create(Enumerable, (function() { var key = encodeURIComponent(pair.key), values = pair.value; if (values && typeof values == 'object') { - if (Object.isArray(values)) - return results.concat(values.map(toQueryPair.curry(key))); + if (Object.isArray(values)) { + var queryValues = []; + for (var i = 0, len = values.length, value; i < len; i++) { + value = values[i]; + queryValues.push(toQueryPair(key, value)); + } + return results.concat(queryValues); + } } else results.push(toQueryPair(key, values)); return results; }).join('&'); @@ -1193,10 +1293,6 @@ var Hash = Class.create(Enumerable, (function() { }).join(', ') + '}>'; } - function toJSON() { - return Object.toJSON(this.toObject()); - } - function clone() { return new Hash(this); } @@ -1216,7 +1312,7 @@ var Hash = Class.create(Enumerable, (function() { update: update, toQueryString: toQueryString, inspect: inspect, - toJSON: toJSON, + toJSON: toObject, clone: clone }; })()); @@ -1241,10 +1337,6 @@ Object.extend(Number.prototype, (function() { return '0'.times(length - string.length) + string; } - function toJSON() { - return isFinite(this) ? this.toString() : 'null'; - } - function abs() { return Math.abs(this); } @@ -1266,7 +1358,6 @@ Object.extend(Number.prototype, (function() { succ: succ, times: times, toPaddedString: toPaddedString, - toJSON: toJSON, abs: abs, round: round, ceil: ceil, @@ -1310,6 +1401,25 @@ var ObjectRange = Class.create(Enumerable, (function() { +var Abstract = { }; + + +var Try = { + these: function() { + var returnValue; + + for (var i = 0, length = arguments.length; i < length; i++) { + var lambda = arguments[i]; + try { + returnValue = lambda(); + break; + } catch (e) { } + } + + return returnValue; + } +}; + var Ajax = { getTransport: function() { return Try.these( @@ -1370,9 +1480,7 @@ Ajax.Base = Class.create({ this.options.method = this.options.method.toLowerCase(); - if (Object.isString(this.options.parameters)) - this.options.parameters = this.options.parameters.toQueryParams(); - else if (Object.isHash(this.options.parameters)) + if (Object.isHash(this.options.parameters)) this.options.parameters = this.options.parameters.toObject(); } }); @@ -1388,22 +1496,21 @@ Ajax.Request = Class.create(Ajax.Base, { request: function(url) { this.url = url; this.method = this.options.method; - var params = Object.clone(this.options.parameters); + var params = Object.isString(this.options.parameters) ? + this.options.parameters : + Object.toQueryString(this.options.parameters); if (!['get', 'post'].include(this.method)) { - params['_method'] = this.method; + params += (params ? '&' : '') + "_method=" + this.method; this.method = 'post'; } - this.parameters = params; - - if (params = Object.toQueryString(params)) { - if (this.method == 'get') - this.url += (this.url.include('?') ? '&' : '?') + params; - else if (/Konqueror|Safari|KHTML/.test(navigator.userAgent)) - params += '&_='; + if (params && this.method === 'get') { + this.url += (this.url.include('?') ? '&' : '?') + params; } + this.parameters = params.toQueryParams(); + try { var response = new Ajax.Response(this); if (this.options.onCreate) this.options.onCreate(response); @@ -1472,11 +1579,12 @@ Ajax.Request = Class.create(Ajax.Base, { success: function() { var status = this.getStatus(); - return !status || (status >= 200 && status < 300); + return !status || (status >= 200 && status < 300) || status == 304; }, getStatus: function() { try { + if (this.transport.status === 1223) return 204; return this.transport.status || 0; } catch (e) { return 0 } }, @@ -1558,14 +1666,14 @@ Ajax.Response = Class.create({ var transport = this.transport = request.transport, readyState = this.readyState = transport.readyState; - if((readyState > 2 && !Prototype.Browser.IE) || readyState == 4) { + if ((readyState > 2 && !Prototype.Browser.IE) || readyState == 4) { this.status = this.getStatus(); this.statusText = this.getStatusText(); this.responseText = String.interpret(transport.responseText); this.headerJSON = this._getHeaderJSON(); } - if(readyState == 4) { + if (readyState == 4) { var xml = transport.responseXML; this.responseXML = Object.isUndefined(xml) ? null : xml; this.responseJSON = this._getResponseJSON(); @@ -1705,7 +1813,6 @@ Ajax.PeriodicalUpdater = Class.create(Ajax.Base, { }); - function $(element) { if (arguments.length > 1) { for (var i = 0, elements = [], length = arguments.length; i < length; i++) @@ -1730,7 +1837,7 @@ if (Prototype.BrowserFeatures.XPath) { /*--------------------------------------------------------------------------*/ -if (!window.Node) var Node = { }; +if (!Node) var Node = { }; if (!Node.ELEMENT_NODE) { Object.extend(Node, { @@ -1750,42 +1857,61 @@ if (!Node.ELEMENT_NODE) { } + (function(global) { + function shouldUseCache(tagName, attributes) { + if (tagName === 'select') return false; + if ('type' in attributes) return false; + return true; + } - var SETATTRIBUTE_IGNORES_NAME = (function(){ - var elForm = document.createElement("form"); - var elInput = document.createElement("input"); - var root = document.documentElement; - elInput.setAttribute("name", "test"); - elForm.appendChild(elInput); - root.appendChild(elForm); - var isBuggy = elForm.elements - ? (typeof elForm.elements.test == "undefined") - : null; - root.removeChild(elForm); - elForm = elInput = null; - return isBuggy; + var HAS_EXTENDED_CREATE_ELEMENT_SYNTAX = (function(){ + try { + var el = document.createElement('<input name="x">'); + return el.tagName.toLowerCase() === 'input' && el.name === 'x'; + } + catch(err) { + return false; + } })(); var element = global.Element; + global.Element = function(tagName, attributes) { attributes = attributes || { }; tagName = tagName.toLowerCase(); var cache = Element.cache; - if (SETATTRIBUTE_IGNORES_NAME && attributes.name) { + + if (HAS_EXTENDED_CREATE_ELEMENT_SYNTAX && attributes.name) { tagName = '<' + tagName + ' name="' + attributes.name + '">'; delete attributes.name; return Element.writeAttribute(document.createElement(tagName), attributes); } + if (!cache[tagName]) cache[tagName] = Element.extend(document.createElement(tagName)); - return Element.writeAttribute(cache[tagName].cloneNode(false), attributes); + + var node = shouldUseCache(tagName, attributes) ? + cache[tagName].cloneNode(false) : document.createElement(tagName); + + return Element.writeAttribute(node, attributes); }; + Object.extend(global.Element, element || { }); if (element) global.Element.prototype = element.prototype; + })(this); -Element.cache = { }; Element.idCounter = 1; +Element.cache = { }; + +Element._purgeElement = function(element) { + var uid = element._prototypeUID; + if (uid) { + Element.stopObserving(element); + element._prototypeUID = void 0; + delete Element.Storage[uid]; + } +} Element.Methods = { visible: function(element) { @@ -1798,7 +1924,6 @@ Element.Methods = { return element; }, - hide: function(element) { element = $(element); element.style.display = 'none'; @@ -1844,6 +1969,21 @@ Element.Methods = { } })(); + var LINK_ELEMENT_INNERHTML_BUGGY = (function() { + try { + var el = document.createElement('div'); + el.innerHTML = "<link>"; + var isBuggy = (el.childNodes.length === 0); + el = null; + return isBuggy; + } catch(e) { + return true; + } + })(); + + var ANY_INNERHTML_BUGGY = SELECT_ELEMENT_INNERHTML_BUGGY || + TABLE_ELEMENT_INNERHTML_BUGGY || LINK_ELEMENT_INNERHTML_BUGGY; + var SCRIPT_ELEMENT_REJECTS_TEXTNODE_APPENDING = (function () { var s = document.createElement("script"), isBuggy = false; @@ -1858,8 +1998,14 @@ Element.Methods = { return isBuggy; })(); + function update(element, content) { element = $(element); + var purgeElement = Element._purgeElement; + + var descendants = element.getElementsByTagName('*'), + i = descendants.length; + while (i--) purgeElement(descendants[i]); if (content && content.toElement) content = content.toElement(); @@ -1876,7 +2022,7 @@ Element.Methods = { return element; } - if (SELECT_ELEMENT_INNERHTML_BUGGY || TABLE_ELEMENT_INNERHTML_BUGGY) { + if (ANY_INNERHTML_BUGGY) { if (tagName in Element._insertionTranslations.tags) { while (element.firstChild) { element.removeChild(element.firstChild); @@ -1885,6 +2031,12 @@ Element.Methods = { .each(function(node) { element.appendChild(node) }); + } else if (LINK_ELEMENT_INNERHTML_BUGGY && Object.isString(content) && content.indexOf('<link') > -1) { + while (element.firstChild) { + element.removeChild(element.firstChild); + } + var nodes = Element._getContentFromAnonymousElement(tagName, content.stripScripts(), true); + nodes.each(function(node) { element.appendChild(node) }); } else { element.innerHTML = content.stripScripts(); @@ -1967,19 +2119,26 @@ Element.Methods = { element = $(element); var result = '<' + element.tagName.toLowerCase(); $H({'id': 'id', 'className': 'class'}).each(function(pair) { - var property = pair.first(), attribute = pair.last(); - var value = (element[property] || '').toString(); + var property = pair.first(), + attribute = pair.last(), + value = (element[property] || '').toString(); if (value) result += ' ' + attribute + '=' + value.inspect(true); }); return result + '>'; }, - recursivelyCollect: function(element, property) { + recursivelyCollect: function(element, property, maximumLength) { element = $(element); + maximumLength = maximumLength || -1; var elements = []; - while (element = element[property]) + + while (element = element[property]) { if (element.nodeType == 1) elements.push(Element.extend(element)); + if (elements.length == maximumLength) + break; + } + return elements; }, @@ -1998,13 +2157,17 @@ Element.Methods = { }, immediateDescendants: function(element) { - if (!(element = $(element).firstChild)) return []; - while (element && element.nodeType != 1) element = element.nextSibling; - if (element) return [element].concat($(element).nextSiblings()); - return []; + var results = [], child = $(element).firstChild; + while (child) { + if (child.nodeType === 1) { + results.push(Element.extend(child)); + } + child = child.nextSibling; + } + return results; }, - previousSiblings: function(element) { + previousSiblings: function(element, maximumLength) { return Element.recursivelyCollect(element, 'previousSibling'); }, @@ -2019,9 +2182,10 @@ Element.Methods = { }, match: function(element, selector) { + element = $(element); if (Object.isString(selector)) - selector = new Selector(selector); - return selector.match($(element)); + return Prototype.Selector.match(element, selector); + return selector.match(element); }, up: function(element, expression, index) { @@ -2029,7 +2193,7 @@ Element.Methods = { if (arguments.length == 1) return $(element.parentNode); var ancestors = Element.ancestors(element); return Object.isNumber(expression) ? ancestors[expression] : - Selector.findElement(ancestors, expression, index); + Prototype.Selector.find(ancestors, expression, index); }, down: function(element, expression, index) { @@ -2041,29 +2205,40 @@ Element.Methods = { previous: function(element, expression, index) { element = $(element); - if (arguments.length == 1) return $(Selector.handlers.previousElementSibling(element)); - var previousSiblings = Element.previousSiblings(element); - return Object.isNumber(expression) ? previousSiblings[expression] : - Selector.findElement(previousSiblings, expression, index); + if (Object.isNumber(expression)) index = expression, expression = false; + if (!Object.isNumber(index)) index = 0; + + if (expression) { + return Prototype.Selector.find(element.previousSiblings(), expression, index); + } else { + return element.recursivelyCollect("previousSibling", index + 1)[index]; + } }, next: function(element, expression, index) { element = $(element); - if (arguments.length == 1) return $(Selector.handlers.nextElementSibling(element)); - var nextSiblings = Element.nextSiblings(element); - return Object.isNumber(expression) ? nextSiblings[expression] : - Selector.findElement(nextSiblings, expression, index); + if (Object.isNumber(expression)) index = expression, expression = false; + if (!Object.isNumber(index)) index = 0; + + if (expression) { + return Prototype.Selector.find(element.nextSiblings(), expression, index); + } else { + var maximumLength = Object.isNumber(index) ? index + 1 : 1; + return element.recursivelyCollect("nextSibling", index + 1)[index]; + } }, select: function(element) { - var args = Array.prototype.slice.call(arguments, 1); - return Selector.findChildElements(element, args); + element = $(element); + var expressions = Array.prototype.slice.call(arguments, 1).join(', '); + return Prototype.Selector.select(expressions, element); }, adjacent: function(element) { - var args = Array.prototype.slice.call(arguments, 1); - return Selector.findChildElements(element.parentNode, args).without(element); + element = $(element); + var expressions = Array.prototype.slice.call(arguments, 1).join(', '); + return Prototype.Selector.select(expressions, element.parentNode).without(element); }, identify: function(element) { @@ -2227,28 +2402,6 @@ Element.Methods = { return element; }, - getDimensions: function(element) { - element = $(element); - var display = Element.getStyle(element, 'display'); - if (display != 'none' && display != null) // Safari bug - return {width: element.offsetWidth, height: element.offsetHeight}; - - var els = element.style; - var originalVisibility = els.visibility; - var originalPosition = els.position; - var originalDisplay = els.display; - els.visibility = 'hidden'; - if (originalPosition != 'fixed') // Switching fixed to absolute causes issues in Safari - els.position = 'absolute'; - els.display = 'block'; - var originalWidth = element.clientWidth; - var originalHeight = element.clientHeight; - els.display = originalDisplay; - els.position = originalPosition; - els.visibility = originalVisibility; - return {width: originalWidth, height: originalHeight}; - }, - makePositioned: function(element) { element = $(element); var pos = Element.getStyle(element, 'position'); @@ -2293,114 +2446,6 @@ Element.Methods = { return element; }, - cumulativeOffset: function(element) { - var valueT = 0, valueL = 0; - do { - valueT += element.offsetTop || 0; - valueL += element.offsetLeft || 0; - element = element.offsetParent; - } while (element); - return Element._returnOffset(valueL, valueT); - }, - - positionedOffset: function(element) { - var valueT = 0, valueL = 0; - do { - valueT += element.offsetTop || 0; - valueL += element.offsetLeft || 0; - element = element.offsetParent; - if (element) { - if (element.tagName.toUpperCase() == 'BODY') break; - var p = Element.getStyle(element, 'position'); - if (p !== 'static') break; - } - } while (element); - return Element._returnOffset(valueL, valueT); - }, - - absolutize: function(element) { - element = $(element); - if (Element.getStyle(element, 'position') == 'absolute') return element; - - var offsets = Element.positionedOffset(element); - var top = offsets[1]; - var left = offsets[0]; - var width = element.clientWidth; - var height = element.clientHeight; - - element._originalLeft = left - parseFloat(element.style.left || 0); - element._originalTop = top - parseFloat(element.style.top || 0); - element._originalWidth = element.style.width; - element._originalHeight = element.style.height; - - element.style.position = 'absolute'; - element.style.top = top + 'px'; - element.style.left = left + 'px'; - element.style.width = width + 'px'; - element.style.height = height + 'px'; - return element; - }, - - relativize: function(element) { - element = $(element); - if (Element.getStyle(element, 'position') == 'relative') return element; - - element.style.position = 'relative'; - var top = parseFloat(element.style.top || 0) - (element._originalTop || 0); - var left = parseFloat(element.style.left || 0) - (element._originalLeft || 0); - - element.style.top = top + 'px'; - element.style.left = left + 'px'; - element.style.height = element._originalHeight; - element.style.width = element._originalWidth; - return element; - }, - - cumulativeScrollOffset: function(element) { - var valueT = 0, valueL = 0; - do { - valueT += element.scrollTop || 0; - valueL += element.scrollLeft || 0; - element = element.parentNode; - } while (element); - return Element._returnOffset(valueL, valueT); - }, - - getOffsetParent: function(element) { - if (element.offsetParent) return $(element.offsetParent); - if (element == document.body) return $(element); - - while ((element = element.parentNode) && element != document.body) - if (Element.getStyle(element, 'position') != 'static') - return $(element); - - return $(document.body); - }, - - viewportOffset: function(forElement) { - var valueT = 0, valueL = 0; - - var element = forElement; - do { - valueT += element.offsetTop || 0; - valueL += element.offsetLeft || 0; - - if (element.offsetParent == document.body && - Element.getStyle(element, 'position') == 'absolute') break; - - } while (element = element.offsetParent); - - element = forElement; - do { - if (!Prototype.Browser.Opera || (element.tagName && (element.tagName.toUpperCase() == 'BODY'))) { - valueT -= element.scrollTop || 0; - valueL -= element.scrollLeft || 0; - } - } while (element = element.parentNode); - - return Element._returnOffset(valueL, valueT); - }, - clonePosition: function(element, source) { var options = Object.extend({ setLeft: true, @@ -2412,11 +2457,10 @@ Element.Methods = { }, arguments[2] || { }); source = $(source); - var p = Element.viewportOffset(source); + var p = Element.viewportOffset(source), delta = [0, 0], parent = null; element = $(element); - var delta = [0, 0]; - var parent = null; + if (Element.getStyle(element, 'position') == 'absolute') { parent = Element.getOffsetParent(element); delta = Element.viewportOffset(parent); @@ -2455,8 +2499,6 @@ if (Prototype.Browser.Opera) { Element.Methods.getStyle = Element.Methods.getStyle.wrap( function(proceed, element, style) { switch (style) { - case 'left': case 'top': case 'right': case 'bottom': - if (proceed(element, 'position') === 'static') return null; case 'height': case 'width': if (!Element.visible(element)) return null; @@ -2492,47 +2534,6 @@ if (Prototype.Browser.Opera) { } else if (Prototype.Browser.IE) { - Element.Methods.getOffsetParent = Element.Methods.getOffsetParent.wrap( - function(proceed, element) { - element = $(element); - try { element.offsetParent } - catch(e) { return $(document.body) } - var position = element.getStyle('position'); - if (position !== 'static') return proceed(element); - element.setStyle({ position: 'relative' }); - var value = proceed(element); - element.setStyle({ position: position }); - return value; - } - ); - - $w('positionedOffset viewportOffset').each(function(method) { - Element.Methods[method] = Element.Methods[method].wrap( - function(proceed, element) { - element = $(element); - try { element.offsetParent } - catch(e) { return Element._returnOffset(0,0) } - var position = element.getStyle('position'); - if (position !== 'static') return proceed(element); - var offsetParent = element.getOffsetParent(); - if (offsetParent && offsetParent.getStyle('position') === 'fixed') - offsetParent.setStyle({ zoom: 1 }); - element.setStyle({ position: 'relative' }); - var value = proceed(element); - element.setStyle({ position: position }); - return value; - } - ); - }); - - Element.Methods.cumulativeOffset = Element.Methods.cumulativeOffset.wrap( - function(proceed, element) { - try { element.offsetParent } - catch(e) { return Element._returnOffset(0,0) } - return proceed(element); - } - ); - Element.Methods.getStyle = function(element, style) { element = $(element); style = (style == 'float' || style == 'cssFloat') ? 'styleFloat' : style.camelize(); @@ -2576,10 +2577,9 @@ else if (Prototype.Browser.IE) { Element._attributeTranslations = (function(){ - var classProp = 'className'; - var forProp = 'for'; - - var el = document.createElement('div'); + var classProp = 'className', + forProp = 'for', + el = document.createElement('div'); el.setAttribute(classProp, 'x'); @@ -2622,10 +2622,9 @@ else if (Prototype.Browser.IE) { }, _getEv: (function(){ - var el = document.createElement('div'); + var el = document.createElement('div'), f; el.onclick = Prototype.emptyFunction; var value = el.getAttribute('onclick'); - var f; if (String(value).indexOf('{') > -1) { f = function(element, attribute) { @@ -2753,7 +2752,7 @@ else if (Prototype.Browser.WebKit) { (value < 0.00001) ? 0 : value; if (value == 1) - if(element.tagName.toUpperCase() == 'IMG' && element.width) { + if (element.tagName.toUpperCase() == 'IMG' && element.width) { element.width++; element.width--; } else try { var n = document.createTextNode(' '); @@ -2763,20 +2762,6 @@ else if (Prototype.Browser.WebKit) { return element; }; - - Element.Methods.cumulativeOffset = function(element) { - var valueT = 0, valueL = 0; - do { - valueT += element.offsetTop || 0; - valueL += element.offsetLeft || 0; - if (element.offsetParent == document.body) - if (Element.getStyle(element, 'position') == 'absolute') break; - - element = element.offsetParent; - } while (element); - - return Element._returnOffset(valueL, valueT); - }; } if ('outerHTML' in document.documentElement) { @@ -2793,8 +2778,8 @@ if ('outerHTML' in document.documentElement) { var parent = element.parentNode, tagName = parent.tagName.toUpperCase(); if (Element._insertionTranslations.tags[tagName]) { - var nextSibling = element.next(); - var fragments = Element._getContentFromAnonymousElement(tagName, content.stripScripts()); + var nextSibling = element.next(), + fragments = Element._getContentFromAnonymousElement(tagName, content.stripScripts()); parent.removeChild(element); if (nextSibling) fragments.each(function(node) { parent.insertBefore(node, nextSibling) }); @@ -2815,12 +2800,27 @@ Element._returnOffset = function(l, t) { return result; }; -Element._getContentFromAnonymousElement = function(tagName, html) { - var div = new Element('div'), t = Element._insertionTranslations.tags[tagName]; - if (t) { - div.innerHTML = t[0] + html + t[1]; - t[2].times(function() { div = div.firstChild }); - } else div.innerHTML = html; +Element._getContentFromAnonymousElement = function(tagName, html, force) { + var div = new Element('div'), + t = Element._insertionTranslations.tags[tagName]; + + var workaround = false; + if (t) workaround = true; + else if (force) { + workaround = true; + t = ['', '', 0]; + } + + if (workaround) { + div.innerHTML = ' ' + t[0] + html + t[1]; + div.removeChild(div.firstChild); + for (var i = t[2]; i--; ) { + div = div.firstChild; + } + } + else { + div.innerHTML = html; + } return $A(div.childNodes); }; @@ -2877,7 +2877,7 @@ Object.extend(Element, Element.Methods); div = null; -})(document.createElement('div')) +})(document.createElement('div')); Element.extend = (function() { @@ -2885,8 +2885,8 @@ Element.extend = (function() { if (typeof window.Element != 'undefined') { var proto = window.Element.prototype; if (proto) { - var id = '_' + (Math.random()+'').slice(2); - var el = document.createElement(tagName); + var id = '_' + (Math.random()+'').slice(2), + el = document.createElement(tagName); proto[id] = 'x'; var isBuggy = (el[id] !== 'x'); delete proto[id]; @@ -2953,10 +2953,14 @@ Element.extend = (function() { return extend; })(); -Element.hasAttribute = function(element, attribute) { - if (element.hasAttribute) return element.hasAttribute(attribute); - return Element.Methods.Simulated.hasAttribute(element, attribute); -}; +if (document.documentElement.hasAttribute) { + Element.hasAttribute = function(element, attribute) { + return element.hasAttribute(attribute); + }; +} +else { + Element.hasAttribute = Element.Methods.Simulated.hasAttribute; +} Element.addMethods = function(methods) { var F = Prototype.BrowserFeatures, T = Element.Methods.ByTag; @@ -2968,7 +2972,8 @@ Element.addMethods = function(methods) { "FORM": Object.clone(Form.Methods), "INPUT": Object.clone(Form.Element.Methods), "SELECT": Object.clone(Form.Element.Methods), - "TEXTAREA": Object.clone(Form.Element.Methods) + "TEXTAREA": Object.clone(Form.Element.Methods), + "BUTTON": Object.clone(Form.Element.Methods) }); } @@ -3020,8 +3025,9 @@ Element.addMethods = function(methods) { klass = 'HTML' + tagName.capitalize() + 'Element'; if (window[klass]) return window[klass]; - var element = document.createElement(tagName); - var proto = element['__proto__'] || element.constructor.prototype; + var element = document.createElement(tagName), + proto = element['__proto__'] || element.constructor.prototype; + element = null; return proto; } @@ -3104,8 +3110,8 @@ Element.addMethods({ uid = 0; } else { if (typeof element._prototypeUID === "undefined") - element._prototypeUID = [Element.Storage.UID++]; - uid = element._prototypeUID[0]; + element._prototypeUID = Element.Storage.UID++; + uid = element._prototypeUID; } if (!Element.Storage[uid]) @@ -3150,770 +3156,1809 @@ Element.addMethods({ } } return Element.extend(clone); - } -}); -/* Portions of the Selector class are derived from Jack Slocum's DomQuery, - * part of YUI-Ext version 0.40, distributed under the terms of an MIT-style - * license. Please see http://www.yui-ext.com/ for more information. */ - -var Selector = Class.create({ - initialize: function(expression) { - this.expression = expression.strip(); - - if (this.shouldUseSelectorsAPI()) { - this.mode = 'selectorsAPI'; - } else if (this.shouldUseXPath()) { - this.mode = 'xpath'; - this.compileXPathMatcher(); - } else { - this.mode = "normal"; - this.compileMatcher(); - } - }, - shouldUseXPath: (function() { - - var IS_DESCENDANT_SELECTOR_BUGGY = (function(){ - var isBuggy = false; - if (document.evaluate && window.XPathResult) { - var el = document.createElement('div'); - el.innerHTML = '<ul><li></li></ul><div><ul><li></li></ul></div>'; - - var xpath = ".//*[local-name()='ul' or local-name()='UL']" + - "//*[local-name()='li' or local-name()='LI']"; - - var result = document.evaluate(xpath, el, null, - XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null); + purge: function(element) { + if (!(element = $(element))) return; + var purgeElement = Element._purgeElement; - isBuggy = (result.snapshotLength !== 2); - el = null; - } - return isBuggy; - })(); + purgeElement(element); - return function() { - if (!Prototype.BrowserFeatures.XPath) return false; + var descendants = element.getElementsByTagName('*'), + i = descendants.length; - var e = this.expression; + while (i--) purgeElement(descendants[i]); - if (Prototype.Browser.WebKit && - (e.include("-of-type") || e.include(":empty"))) - return false; + return null; + } +}); - if ((/(\[[\w-]*?:|:checked)/).test(e)) - return false; +(function() { - if (IS_DESCENDANT_SELECTOR_BUGGY) return false; + function toDecimal(pctString) { + var match = pctString.match(/^(\d+)%?$/i); + if (!match) return null; + return (Number(match[1]) / 100); + } - return true; + function getPixelValue(value, property, context) { + var element = null; + if (Object.isElement(value)) { + element = value; + value = element.getStyle(property); } - })(), + if (value === null) { + return null; + } - shouldUseSelectorsAPI: function() { - if (!Prototype.BrowserFeatures.SelectorsAPI) return false; + if ((/^(?:-)?\d+(\.\d+)?(px)?$/i).test(value)) { + return window.parseFloat(value); + } - if (Selector.CASE_INSENSITIVE_CLASS_NAMES) return false; + var isPercentage = value.include('%'), isViewport = (context === document.viewport); - if (!Selector._div) Selector._div = new Element('div'); + if (/\d/.test(value) && element && element.runtimeStyle && !(isPercentage && isViewport)) { + var style = element.style.left, rStyle = element.runtimeStyle.left; + element.runtimeStyle.left = element.currentStyle.left; + element.style.left = value || 0; + value = element.style.pixelLeft; + element.style.left = style; + element.runtimeStyle.left = rStyle; - try { - Selector._div.querySelector(this.expression); - } catch(e) { - return false; + return value; } - return true; - }, + if (element && isPercentage) { + context = context || element.parentNode; + var decimal = toDecimal(value); + var whole = null; + var position = element.getStyle('position'); - compileMatcher: function() { - var e = this.expression, ps = Selector.patterns, h = Selector.handlers, - c = Selector.criteria, le, p, m, len = ps.length, name; + var isHorizontal = property.include('left') || property.include('right') || + property.include('width'); - if (Selector._cache[e]) { - this.matcher = Selector._cache[e]; - return; - } - - this.matcher = ["this.matcher = function(root) {", - "var r = root, h = Selector.handlers, c = false, n;"]; + var isVertical = property.include('top') || property.include('bottom') || + property.include('height'); - while (e && le != e && (/\S/).test(e)) { - le = e; - for (var i = 0; i<len; i++) { - p = ps[i].re; - name = ps[i].name; - if (m = e.match(p)) { - this.matcher.push(Object.isFunction(c[name]) ? c[name](m) : - new Template(c[name]).evaluate(m)); - e = e.replace(m[0], ''); - break; + if (context === document.viewport) { + if (isHorizontal) { + whole = document.viewport.getWidth(); + } else if (isVertical) { + whole = document.viewport.getHeight(); + } + } else { + if (isHorizontal) { + whole = $(context).measure('width'); + } else if (isVertical) { + whole = $(context).measure('height'); } } - } - this.matcher.push("return h.unique(n);\n}"); - eval(this.matcher.join('\n')); - Selector._cache[this.expression] = this.matcher; - }, + return (whole === null) ? 0 : whole * decimal; + } - compileXPathMatcher: function() { - var e = this.expression, ps = Selector.patterns, - x = Selector.xpath, le, m, len = ps.length, name; + return 0; + } - if (Selector._cache[e]) { - this.xpath = Selector._cache[e]; return; + function toCSSPixels(number) { + if (Object.isString(number) && number.endsWith('px')) { + return number; } + return number + 'px'; + } - this.matcher = ['.//*']; - while (e && le != e && (/\S/).test(e)) { - le = e; - for (var i = 0; i<len; i++) { - name = ps[i].name; - if (m = e.match(ps[i].re)) { - this.matcher.push(Object.isFunction(x[name]) ? x[name](m) : - new Template(x[name]).evaluate(m)); - e = e.replace(m[0], ''); - break; - } + function isDisplayed(element) { + var originalElement = element; + while (element && element.parentNode) { + var display = element.getStyle('display'); + if (display === 'none') { + return false; } + element = $(element.parentNode); } + return true; + } - this.xpath = this.matcher.join(''); - Selector._cache[this.expression] = this.xpath; - }, + var hasLayout = Prototype.K; + if ('currentStyle' in document.documentElement) { + hasLayout = function(element) { + if (!element.currentStyle.hasLayout) { + element.style.zoom = 1; + } + return element; + }; + } - findElements: function(root) { - root = root || document; - var e = this.expression, results; + function cssNameFor(key) { + if (key.include('border')) key = key + '-width'; + return key.camelize(); + } - switch (this.mode) { - case 'selectorsAPI': - if (root !== document) { - var oldId = root.id, id = $(root).identify(); - id = id.replace(/([\.:])/g, "\\$1"); - e = "#" + id + " " + e; - } + Element.Layout = Class.create(Hash, { + initialize: function($super, element, preCompute) { + $super(); + this.element = $(element); - results = $A(root.querySelectorAll(e)).map(Element.extend); - root.id = oldId; + Element.Layout.PROPERTIES.each( function(property) { + this._set(property, null); + }, this); - return results; - case 'xpath': - return document._getElementsByXPath(this.xpath, root); - default: - return this.matcher(root); - } - }, + if (preCompute) { + this._preComputing = true; + this._begin(); + Element.Layout.PROPERTIES.each( this._compute, this ); + this._end(); + this._preComputing = false; + } + }, - match: function(element) { - this.tokens = []; - - var e = this.expression, ps = Selector.patterns, as = Selector.assertions; - var le, p, m, len = ps.length, name; - - while (e && le !== e && (/\S/).test(e)) { - le = e; - for (var i = 0; i<len; i++) { - p = ps[i].re; - name = ps[i].name; - if (m = e.match(p)) { - if (as[name]) { - this.tokens.push([name, Object.clone(m)]); - e = e.replace(m[0], ''); - } else { - return this.findElements(document).include(element); - } - } + _set: function(property, value) { + return Hash.prototype.set.call(this, property, value); + }, + + set: function(property, value) { + throw "Properties of Element.Layout are read-only."; + }, + + get: function($super, property) { + var value = $super(property); + return value === null ? this._compute(property) : value; + }, + + _begin: function() { + if (this._prepared) return; + + var element = this.element; + if (isDisplayed(element)) { + this._prepared = true; + return; } - } - var match = true, name, matches; - for (var i = 0, token; token = this.tokens[i]; i++) { - name = token[0], matches = token[1]; - if (!Selector.assertions[name](element, matches)) { - match = false; break; + var originalStyles = { + position: element.style.position || '', + width: element.style.width || '', + visibility: element.style.visibility || '', + display: element.style.display || '' + }; + + element.store('prototype_original_styles', originalStyles); + + var position = element.getStyle('position'), + width = element.getStyle('width'); + + if (width === "0px" || width === null) { + element.style.display = 'block'; + width = element.getStyle('width'); } - } - return match; - }, + var context = (position === 'fixed') ? document.viewport : + element.parentNode; - toString: function() { - return this.expression; - }, + element.setStyle({ + position: 'absolute', + visibility: 'hidden', + display: 'block' + }); - inspect: function() { - return "#<Selector:" + this.expression.inspect() + ">"; - } -}); + var positionedWidth = element.getStyle('width'); -if (Prototype.BrowserFeatures.SelectorsAPI && - document.compatMode === 'BackCompat') { - Selector.CASE_INSENSITIVE_CLASS_NAMES = (function(){ - var div = document.createElement('div'), - span = document.createElement('span'); - - div.id = "prototype_test_id"; - span.className = 'Test'; - div.appendChild(span); - var isIgnored = (div.querySelector('#prototype_test_id .test') !== null); - div = span = null; - return isIgnored; - })(); -} + var newWidth; + if (width && (positionedWidth === width)) { + newWidth = getPixelValue(element, 'width', context); + } else if (position === 'absolute' || position === 'fixed') { + newWidth = getPixelValue(element, 'width', context); + } else { + var parent = element.parentNode, pLayout = $(parent).getLayout(); + + newWidth = pLayout.get('width') - + this.get('margin-left') - + this.get('border-left') - + this.get('padding-left') - + this.get('padding-right') - + this.get('border-right') - + this.get('margin-right'); + } -Object.extend(Selector, { - _cache: { }, - - xpath: { - descendant: "//*", - child: "/*", - adjacent: "/following-sibling::*[1]", - laterSibling: '/following-sibling::*', - tagName: function(m) { - if (m[1] == '*') return ''; - return "[local-name()='" + m[1].toLowerCase() + - "' or local-name()='" + m[1].toUpperCase() + "']"; + element.setStyle({ width: newWidth + 'px' }); + + this._prepared = true; }, - className: "[contains(concat(' ', @class, ' '), ' #{1} ')]", - id: "[@id='#{1}']", - attrPresence: function(m) { - m[1] = m[1].toLowerCase(); - return new Template("[@#{1}]").evaluate(m); + + _end: function() { + var element = this.element; + var originalStyles = element.retrieve('prototype_original_styles'); + element.store('prototype_original_styles', null); + element.setStyle(originalStyles); + this._prepared = false; }, - attr: function(m) { - m[1] = m[1].toLowerCase(); - m[3] = m[5] || m[6]; - return new Template(Selector.xpath.operators[m[2]]).evaluate(m); + + _compute: function(property) { + var COMPUTATIONS = Element.Layout.COMPUTATIONS; + if (!(property in COMPUTATIONS)) { + throw "Property not found."; + } + + return this._set(property, COMPUTATIONS[property].call(this, this.element)); }, - pseudo: function(m) { - var h = Selector.xpath.pseudos[m[1]]; - if (!h) return ''; - if (Object.isFunction(h)) return h(m); - return new Template(Selector.xpath.pseudos[m[1]]).evaluate(m); + + toObject: function() { + var args = $A(arguments); + var keys = (args.length === 0) ? Element.Layout.PROPERTIES : + args.join(' ').split(' '); + var obj = {}; + keys.each( function(key) { + if (!Element.Layout.PROPERTIES.include(key)) return; + var value = this.get(key); + if (value != null) obj[key] = value; + }, this); + return obj; }, - operators: { - '=': "[@#{1}='#{3}']", - '!=': "[@#{1}!='#{3}']", - '^=': "[starts-with(@#{1}, '#{3}')]", - '$=': "[substring(@#{1}, (string-length(@#{1}) - string-length('#{3}') + 1))='#{3}']", - '*=': "[contains(@#{1}, '#{3}')]", - '~=': "[contains(concat(' ', @#{1}, ' '), ' #{3} ')]", - '|=': "[contains(concat('-', @#{1}, '-'), '-#{3}-')]" + + toHash: function() { + var obj = this.toObject.apply(this, arguments); + return new Hash(obj); }, - pseudos: { - 'first-child': '[not(preceding-sibling::*)]', - 'last-child': '[not(following-sibling::*)]', - 'only-child': '[not(preceding-sibling::* or following-sibling::*)]', - 'empty': "[count(*) = 0 and (count(text()) = 0)]", - 'checked': "[@checked]", - 'disabled': "[(@disabled) and (@type!='hidden')]", - 'enabled': "[not(@disabled) and (@type!='hidden')]", - 'not': function(m) { - var e = m[6], p = Selector.patterns, - x = Selector.xpath, le, v, len = p.length, name; - - var exclusion = []; - while (e && le != e && (/\S/).test(e)) { - le = e; - for (var i = 0; i<len; i++) { - name = p[i].name - if (m = e.match(p[i].re)) { - v = Object.isFunction(x[name]) ? x[name](m) : new Template(x[name]).evaluate(m); - exclusion.push("(" + v.substring(1, v.length - 1) + ")"); - e = e.replace(m[0], ''); - break; - } - } + + toCSS: function() { + var args = $A(arguments); + var keys = (args.length === 0) ? Element.Layout.PROPERTIES : + args.join(' ').split(' '); + var css = {}; + + keys.each( function(key) { + if (!Element.Layout.PROPERTIES.include(key)) return; + if (Element.Layout.COMPOSITE_PROPERTIES.include(key)) return; + + var value = this.get(key); + if (value != null) css[cssNameFor(key)] = value + 'px'; + }, this); + return css; + }, + + inspect: function() { + return "#<Element.Layout>"; + } + }); + + Object.extend(Element.Layout, { + PROPERTIES: $w('height width top left right bottom border-left border-right border-top border-bottom padding-left padding-right padding-top padding-bottom margin-top margin-bottom margin-left margin-right padding-box-width padding-box-height border-box-width border-box-height margin-box-width margin-box-height'), + + COMPOSITE_PROPERTIES: $w('padding-box-width padding-box-height margin-box-width margin-box-height border-box-width border-box-height'), + + COMPUTATIONS: { + 'height': function(element) { + if (!this._preComputing) this._begin(); + + var bHeight = this.get('border-box-height'); + if (bHeight <= 0) { + if (!this._preComputing) this._end(); + return 0; + } + + var bTop = this.get('border-top'), + bBottom = this.get('border-bottom'); + + var pTop = this.get('padding-top'), + pBottom = this.get('padding-bottom'); + + if (!this._preComputing) this._end(); + + return bHeight - bTop - bBottom - pTop - pBottom; + }, + + 'width': function(element) { + if (!this._preComputing) this._begin(); + + var bWidth = this.get('border-box-width'); + if (bWidth <= 0) { + if (!this._preComputing) this._end(); + return 0; } - return "[not(" + exclusion.join(" and ") + ")]"; + + var bLeft = this.get('border-left'), + bRight = this.get('border-right'); + + var pLeft = this.get('padding-left'), + pRight = this.get('padding-right'); + + if (!this._preComputing) this._end(); + + return bWidth - bLeft - bRight - pLeft - pRight; + }, + + 'padding-box-height': function(element) { + var height = this.get('height'), + pTop = this.get('padding-top'), + pBottom = this.get('padding-bottom'); + + return height + pTop + pBottom; + }, + + 'padding-box-width': function(element) { + var width = this.get('width'), + pLeft = this.get('padding-left'), + pRight = this.get('padding-right'); + + return width + pLeft + pRight; + }, + + 'border-box-height': function(element) { + if (!this._preComputing) this._begin(); + var height = element.offsetHeight; + if (!this._preComputing) this._end(); + return height; }, - 'nth-child': function(m) { - return Selector.xpath.pseudos.nth("(count(./preceding-sibling::*) + 1) ", m); + + 'border-box-width': function(element) { + if (!this._preComputing) this._begin(); + var width = element.offsetWidth; + if (!this._preComputing) this._end(); + return width; + }, + + 'margin-box-height': function(element) { + var bHeight = this.get('border-box-height'), + mTop = this.get('margin-top'), + mBottom = this.get('margin-bottom'); + + if (bHeight <= 0) return 0; + + return bHeight + mTop + mBottom; + }, + + 'margin-box-width': function(element) { + var bWidth = this.get('border-box-width'), + mLeft = this.get('margin-left'), + mRight = this.get('margin-right'); + + if (bWidth <= 0) return 0; + + return bWidth + mLeft + mRight; + }, + + 'top': function(element) { + var offset = element.positionedOffset(); + return offset.top; + }, + + 'bottom': function(element) { + var offset = element.positionedOffset(), + parent = element.getOffsetParent(), + pHeight = parent.measure('height'); + + var mHeight = this.get('border-box-height'); + + return pHeight - mHeight - offset.top; + }, + + 'left': function(element) { + var offset = element.positionedOffset(); + return offset.left; + }, + + 'right': function(element) { + var offset = element.positionedOffset(), + parent = element.getOffsetParent(), + pWidth = parent.measure('width'); + + var mWidth = this.get('border-box-width'); + + return pWidth - mWidth - offset.left; }, - 'nth-last-child': function(m) { - return Selector.xpath.pseudos.nth("(count(./following-sibling::*) + 1) ", m); + + 'padding-top': function(element) { + return getPixelValue(element, 'paddingTop'); }, - 'nth-of-type': function(m) { - return Selector.xpath.pseudos.nth("position() ", m); + + 'padding-bottom': function(element) { + return getPixelValue(element, 'paddingBottom'); }, - 'nth-last-of-type': function(m) { - return Selector.xpath.pseudos.nth("(last() + 1 - position()) ", m); + + 'padding-left': function(element) { + return getPixelValue(element, 'paddingLeft'); }, - 'first-of-type': function(m) { - m[6] = "1"; return Selector.xpath.pseudos['nth-of-type'](m); + + 'padding-right': function(element) { + return getPixelValue(element, 'paddingRight'); }, - 'last-of-type': function(m) { - m[6] = "1"; return Selector.xpath.pseudos['nth-last-of-type'](m); + + 'border-top': function(element) { + return getPixelValue(element, 'borderTopWidth'); }, - 'only-of-type': function(m) { - var p = Selector.xpath.pseudos; return p['first-of-type'](m) + p['last-of-type'](m); + + 'border-bottom': function(element) { + return getPixelValue(element, 'borderBottomWidth'); }, - nth: function(fragment, m) { - var mm, formula = m[6], predicate; - if (formula == 'even') formula = '2n+0'; - if (formula == 'odd') formula = '2n+1'; - if (mm = formula.match(/^(\d+)$/)) // digit only - return '[' + fragment + "= " + mm[1] + ']'; - if (mm = formula.match(/^(-?\d*)?n(([+-])(\d+))?/)) { // an+b - if (mm[1] == "-") mm[1] = -1; - var a = mm[1] ? Number(mm[1]) : 1; - var b = mm[2] ? Number(mm[2]) : 0; - predicate = "[((#{fragment} - #{b}) mod #{a} = 0) and " + - "((#{fragment} - #{b}) div #{a} >= 0)]"; - return new Template(predicate).evaluate({ - fragment: fragment, a: a, b: b }); - } + + 'border-left': function(element) { + return getPixelValue(element, 'borderLeftWidth'); + }, + + 'border-right': function(element) { + return getPixelValue(element, 'borderRightWidth'); + }, + + 'margin-top': function(element) { + return getPixelValue(element, 'marginTop'); + }, + + 'margin-bottom': function(element) { + return getPixelValue(element, 'marginBottom'); + }, + + 'margin-left': function(element) { + return getPixelValue(element, 'marginLeft'); + }, + + 'margin-right': function(element) { + return getPixelValue(element, 'marginRight'); } } - }, + }); - criteria: { - tagName: 'n = h.tagName(n, r, "#{1}", c); c = false;', - className: 'n = h.className(n, r, "#{1}", c); c = false;', - id: 'n = h.id(n, r, "#{1}", c); c = false;', - attrPresence: 'n = h.attrPresence(n, r, "#{1}", c); c = false;', - attr: function(m) { - m[3] = (m[5] || m[6]); - return new Template('n = h.attr(n, r, "#{1}", "#{3}", "#{2}", c); c = false;').evaluate(m); - }, - pseudo: function(m) { - if (m[6]) m[6] = m[6].replace(/"/g, '\\"'); - return new Template('n = h.pseudo(n, "#{1}", "#{6}", r, c); c = false;').evaluate(m); - }, - descendant: 'c = "descendant";', - child: 'c = "child";', - adjacent: 'c = "adjacent";', - laterSibling: 'c = "laterSibling";' - }, + if ('getBoundingClientRect' in document.documentElement) { + Object.extend(Element.Layout.COMPUTATIONS, { + 'right': function(element) { + var parent = hasLayout(element.getOffsetParent()); + var rect = element.getBoundingClientRect(), + pRect = parent.getBoundingClientRect(); + + return (pRect.right - rect.right).round(); + }, + + 'bottom': function(element) { + var parent = hasLayout(element.getOffsetParent()); + var rect = element.getBoundingClientRect(), + pRect = parent.getBoundingClientRect(); + + return (pRect.bottom - rect.bottom).round(); + } + }); + } - patterns: [ - { name: 'laterSibling', re: /^\s*~\s*/ }, - { name: 'child', re: /^\s*>\s*/ }, - { name: 'adjacent', re: /^\s*\+\s*/ }, - { name: 'descendant', re: /^\s/ }, - - { name: 'tagName', re: /^\s*(\*|[\w\-]+)(\b|$)?/ }, - { name: 'id', re: /^#([\w\-\*]+)(\b|$)/ }, - { name: 'className', re: /^\.([\w\-\*]+)(\b|$)/ }, - { name: 'pseudo', re: /^:((first|last|nth|nth-last|only)(-child|-of-type)|empty|checked|(en|dis)abled|not)(\((.*?)\))?(\b|$|(?=\s|[:+~>]))/ }, - { name: 'attrPresence', re: /^\[((?:[\w-]+:)?[\w-]+)\]/ }, - { name: 'attr', re: /\[((?:[\w-]*:)?[\w-]+)\s*(?:([!^$*~|]?=)\s*((['"])([^\4]*?)\4|([^'"][^\]]*?)))?\]/ } - ], - - assertions: { - tagName: function(element, matches) { - return matches[1].toUpperCase() == element.tagName.toUpperCase(); + Element.Offset = Class.create({ + initialize: function(left, top) { + this.left = left.round(); + this.top = top.round(); + + this[0] = this.left; + this[1] = this.top; }, - className: function(element, matches) { - return Element.hasClassName(element, matches[1]); + relativeTo: function(offset) { + return new Element.Offset( + this.left - offset.left, + this.top - offset.top + ); }, - id: function(element, matches) { - return element.id === matches[1]; + inspect: function() { + return "#<Element.Offset left: #{left} top: #{top}>".interpolate(this); }, - attrPresence: function(element, matches) { - return Element.hasAttribute(element, matches[1]); + toString: function() { + return "[#{left}, #{top}]".interpolate(this); }, - attr: function(element, matches) { - var nodeValue = Element.readAttribute(element, matches[1]); - return nodeValue && Selector.operators[matches[2]](nodeValue, matches[5] || matches[6]); + toArray: function() { + return [this.left, this.top]; } - }, + }); - handlers: { - concat: function(a, b) { - for (var i = 0, node; node = b[i]; i++) - a.push(node); - return a; - }, + function getLayout(element, preCompute) { + return new Element.Layout(element, preCompute); + } - mark: function(nodes) { - var _true = Prototype.emptyFunction; - for (var i = 0, node; node = nodes[i]; i++) - node._countedByPrototype = _true; - return nodes; - }, + function measure(element, property) { + return $(element).getLayout().get(property); + } - unmark: (function(){ + function getDimensions(element) { + element = $(element); + var display = Element.getStyle(element, 'display'); - var PROPERTIES_ATTRIBUTES_MAP = (function(){ - var el = document.createElement('div'), - isBuggy = false, - propName = '_countedByPrototype', - value = 'x' - el[propName] = value; - isBuggy = (el.getAttribute(propName) === value); - el = null; - return isBuggy; - })(); - - return PROPERTIES_ATTRIBUTES_MAP ? - function(nodes) { - for (var i = 0, node; node = nodes[i]; i++) - node.removeAttribute('_countedByPrototype'); - return nodes; - } : - function(nodes) { - for (var i = 0, node; node = nodes[i]; i++) - node._countedByPrototype = void 0; - return nodes; - } - })(), + if (display && display !== 'none') { + return { width: element.offsetWidth, height: element.offsetHeight }; + } - index: function(parentNode, reverse, ofType) { - parentNode._countedByPrototype = Prototype.emptyFunction; - if (reverse) { - for (var nodes = parentNode.childNodes, i = nodes.length - 1, j = 1; i >= 0; i--) { - var node = nodes[i]; - if (node.nodeType == 1 && (!ofType || node._countedByPrototype)) node.nodeIndex = j++; - } - } else { - for (var i = 0, j = 1, nodes = parentNode.childNodes; node = nodes[i]; i++) - if (node.nodeType == 1 && (!ofType || node._countedByPrototype)) node.nodeIndex = j++; - } - }, + var style = element.style; + var originalStyles = { + visibility: style.visibility, + position: style.position, + display: style.display + }; - unique: function(nodes) { - if (nodes.length == 0) return nodes; - var results = [], n; - for (var i = 0, l = nodes.length; i < l; i++) - if (typeof (n = nodes[i])._countedByPrototype == 'undefined') { - n._countedByPrototype = Prototype.emptyFunction; - results.push(Element.extend(n)); - } - return Selector.handlers.unmark(results); - }, + var newStyles = { + visibility: 'hidden', + display: 'block' + }; - descendant: function(nodes) { - var h = Selector.handlers; - for (var i = 0, results = [], node; node = nodes[i]; i++) - h.concat(results, node.getElementsByTagName('*')); - return results; - }, + if (originalStyles.position !== 'fixed') + newStyles.position = 'absolute'; - child: function(nodes) { - var h = Selector.handlers; - for (var i = 0, results = [], node; node = nodes[i]; i++) { - for (var j = 0, child; child = node.childNodes[j]; j++) - if (child.nodeType == 1 && child.tagName != '!') results.push(child); - } - return results; - }, + Element.setStyle(element, newStyles); - adjacent: function(nodes) { - for (var i = 0, results = [], node; node = nodes[i]; i++) { - var next = this.nextElementSibling(node); - if (next) results.push(next); - } - return results; - }, + var dimensions = { + width: element.offsetWidth, + height: element.offsetHeight + }; - laterSibling: function(nodes) { - var h = Selector.handlers; - for (var i = 0, results = [], node; node = nodes[i]; i++) - h.concat(results, Element.nextSiblings(node)); - return results; - }, + Element.setStyle(element, originalStyles); - nextElementSibling: function(node) { - while (node = node.nextSibling) - if (node.nodeType == 1) return node; - return null; - }, + return dimensions; + } - previousElementSibling: function(node) { - while (node = node.previousSibling) - if (node.nodeType == 1) return node; - return null; - }, + function getOffsetParent(element) { + element = $(element); - tagName: function(nodes, root, tagName, combinator) { - var uTagName = tagName.toUpperCase(); - var results = [], h = Selector.handlers; - if (nodes) { - if (combinator) { - if (combinator == "descendant") { - for (var i = 0, node; node = nodes[i]; i++) - h.concat(results, node.getElementsByTagName(tagName)); - return results; - } else nodes = this[combinator](nodes); - if (tagName == "*") return nodes; - } - for (var i = 0, node; node = nodes[i]; i++) - if (node.tagName.toUpperCase() === uTagName) results.push(node); - return results; - } else return root.getElementsByTagName(tagName); - }, + if (isDocument(element) || isDetached(element) || isBody(element) || isHtml(element)) + return $(document.body); - id: function(nodes, root, id, combinator) { - var targetNode = $(id), h = Selector.handlers; + var isInline = (Element.getStyle(element, 'display') === 'inline'); + if (!isInline && element.offsetParent) return $(element.offsetParent); - if (root == document) { - if (!targetNode) return []; - if (!nodes) return [targetNode]; - } else { - if (!root.sourceIndex || root.sourceIndex < 1) { - var nodes = root.getElementsByTagName('*'); - for (var j = 0, node; node = nodes[j]; j++) { - if (node.id === id) return [node]; - } - } + while ((element = element.parentNode) && element !== document.body) { + if (Element.getStyle(element, 'position') !== 'static') { + return isHtml(element) ? $(document.body) : $(element); } + } - if (nodes) { - if (combinator) { - if (combinator == 'child') { - for (var i = 0, node; node = nodes[i]; i++) - if (targetNode.parentNode == node) return [targetNode]; - } else if (combinator == 'descendant') { - for (var i = 0, node; node = nodes[i]; i++) - if (Element.descendantOf(targetNode, node)) return [targetNode]; - } else if (combinator == 'adjacent') { - for (var i = 0, node; node = nodes[i]; i++) - if (Selector.handlers.previousElementSibling(targetNode) == node) - return [targetNode]; - } else nodes = h[combinator](nodes); - } - for (var i = 0, node; node = nodes[i]; i++) - if (node == targetNode) return [targetNode]; - return []; - } - return (targetNode && Element.descendantOf(targetNode, root)) ? [targetNode] : []; - }, + return $(document.body); + } - className: function(nodes, root, className, combinator) { - if (nodes && combinator) nodes = this[combinator](nodes); - return Selector.handlers.byClassName(nodes, root, className); - }, - byClassName: function(nodes, root, className) { - if (!nodes) nodes = Selector.handlers.descendant([root]); - var needle = ' ' + className + ' '; - for (var i = 0, results = [], node, nodeClassName; node = nodes[i]; i++) { - nodeClassName = node.className; - if (nodeClassName.length == 0) continue; - if (nodeClassName == className || (' ' + nodeClassName + ' ').include(needle)) - results.push(node); + function cumulativeOffset(element) { + element = $(element); + var valueT = 0, valueL = 0; + if (element.parentNode) { + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + element = element.offsetParent; + } while (element); + } + return new Element.Offset(valueL, valueT); + } + + function positionedOffset(element) { + element = $(element); + + var layout = element.getLayout(); + + var valueT = 0, valueL = 0; + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + element = element.offsetParent; + if (element) { + if (isBody(element)) break; + var p = Element.getStyle(element, 'position'); + if (p !== 'static') break; } - return results; - }, + } while (element); - attrPresence: function(nodes, root, attr, combinator) { - if (!nodes) nodes = root.getElementsByTagName("*"); - if (nodes && combinator) nodes = this[combinator](nodes); - var results = []; - for (var i = 0, node; node = nodes[i]; i++) - if (Element.hasAttribute(node, attr)) results.push(node); - return results; - }, + valueL -= layout.get('margin-top'); + valueT -= layout.get('margin-left'); + + return new Element.Offset(valueL, valueT); + } + + function cumulativeScrollOffset(element) { + var valueT = 0, valueL = 0; + do { + valueT += element.scrollTop || 0; + valueL += element.scrollLeft || 0; + element = element.parentNode; + } while (element); + return new Element.Offset(valueL, valueT); + } + + function viewportOffset(forElement) { + element = $(element); + var valueT = 0, valueL = 0, docBody = document.body; + + var element = forElement; + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + if (element.offsetParent == docBody && + Element.getStyle(element, 'position') == 'absolute') break; + } while (element = element.offsetParent); - attr: function(nodes, root, attr, value, operator, combinator) { - if (!nodes) nodes = root.getElementsByTagName("*"); - if (nodes && combinator) nodes = this[combinator](nodes); - var handler = Selector.operators[operator], results = []; - for (var i = 0, node; node = nodes[i]; i++) { - var nodeValue = Element.readAttribute(node, attr); - if (nodeValue === null) continue; - if (handler(nodeValue, value)) results.push(node); + element = forElement; + do { + if (element != docBody) { + valueT -= element.scrollTop || 0; + valueL -= element.scrollLeft || 0; } - return results; - }, + } while (element = element.parentNode); + return new Element.Offset(valueL, valueT); + } + + function absolutize(element) { + element = $(element); - pseudo: function(nodes, name, value, root, combinator) { - if (nodes && combinator) nodes = this[combinator](nodes); - if (!nodes) nodes = root.getElementsByTagName("*"); - return Selector.pseudos[name](nodes, value, root); + if (Element.getStyle(element, 'position') === 'absolute') { + return element; } - }, - pseudos: { - 'first-child': function(nodes, value, root) { - for (var i = 0, results = [], node; node = nodes[i]; i++) { - if (Selector.handlers.previousElementSibling(node)) continue; - results.push(node); - } - return results; - }, - 'last-child': function(nodes, value, root) { - for (var i = 0, results = [], node; node = nodes[i]; i++) { - if (Selector.handlers.nextElementSibling(node)) continue; - results.push(node); - } - return results; - }, - 'only-child': function(nodes, value, root) { - var h = Selector.handlers; - for (var i = 0, results = [], node; node = nodes[i]; i++) - if (!h.previousElementSibling(node) && !h.nextElementSibling(node)) - results.push(node); - return results; - }, - 'nth-child': function(nodes, formula, root) { - return Selector.pseudos.nth(nodes, formula, root); - }, - 'nth-last-child': function(nodes, formula, root) { - return Selector.pseudos.nth(nodes, formula, root, true); - }, - 'nth-of-type': function(nodes, formula, root) { - return Selector.pseudos.nth(nodes, formula, root, false, true); - }, - 'nth-last-of-type': function(nodes, formula, root) { - return Selector.pseudos.nth(nodes, formula, root, true, true); - }, - 'first-of-type': function(nodes, formula, root) { - return Selector.pseudos.nth(nodes, "1", root, false, true); - }, - 'last-of-type': function(nodes, formula, root) { - return Selector.pseudos.nth(nodes, "1", root, true, true); - }, - 'only-of-type': function(nodes, formula, root) { - var p = Selector.pseudos; - return p['last-of-type'](p['first-of-type'](nodes, formula, root), formula, root); - }, + var offsetParent = getOffsetParent(element); + var eOffset = element.viewportOffset(), + pOffset = offsetParent.viewportOffset(); - getIndices: function(a, b, total) { - if (a == 0) return b > 0 ? [b] : []; - return $R(1, total).inject([], function(memo, i) { - if (0 == (i - b) % a && (i - b) / a >= 0) memo.push(i); - return memo; - }); - }, + var offset = eOffset.relativeTo(pOffset); + var layout = element.getLayout(); - nth: function(nodes, formula, root, reverse, ofType) { - if (nodes.length == 0) return []; - if (formula == 'even') formula = '2n+0'; - if (formula == 'odd') formula = '2n+1'; - var h = Selector.handlers, results = [], indexed = [], m; - h.mark(nodes); - for (var i = 0, node; node = nodes[i]; i++) { - if (!node.parentNode._countedByPrototype) { - h.index(node.parentNode, reverse, ofType); - indexed.push(node.parentNode); - } - } - if (formula.match(/^\d+$/)) { // just a number - formula = Number(formula); - for (var i = 0, node; node = nodes[i]; i++) - if (node.nodeIndex == formula) results.push(node); - } else if (m = formula.match(/^(-?\d*)?n(([+-])(\d+))?/)) { // an+b - if (m[1] == "-") m[1] = -1; - var a = m[1] ? Number(m[1]) : 1; - var b = m[2] ? Number(m[2]) : 0; - var indices = Selector.pseudos.getIndices(a, b, nodes.length); - for (var i = 0, node, l = indices.length; node = nodes[i]; i++) { - for (var j = 0; j < l; j++) - if (node.nodeIndex == indices[j]) results.push(node); - } - } - h.unmark(nodes); - h.unmark(indexed); - return results; - }, + element.store('prototype_absolutize_original_styles', { + left: element.getStyle('left'), + top: element.getStyle('top'), + width: element.getStyle('width'), + height: element.getStyle('height') + }); + + element.setStyle({ + position: 'absolute', + top: offset.top + 'px', + left: offset.left + 'px', + width: layout.get('width') + 'px', + height: layout.get('height') + 'px' + }); + + return element; + } + + function relativize(element) { + element = $(element); + if (Element.getStyle(element, 'position') === 'relative') { + return element; + } + + var originalStyles = + element.retrieve('prototype_absolutize_original_styles'); + + if (originalStyles) element.setStyle(originalStyles); + return element; + } + + if (Prototype.Browser.IE) { + getOffsetParent = getOffsetParent.wrap( + function(proceed, element) { + element = $(element); + + if (isDocument(element) || isDetached(element) || isBody(element) || isHtml(element)) + return $(document.body); + + var position = element.getStyle('position'); + if (position !== 'static') return proceed(element); - 'empty': function(nodes, value, root) { - for (var i = 0, results = [], node; node = nodes[i]; i++) { - if (node.tagName == '!' || node.firstChild) continue; - results.push(node); + element.setStyle({ position: 'relative' }); + var value = proceed(element); + element.setStyle({ position: position }); + return value; } - return results; - }, + ); - 'not': function(nodes, selector, root) { - var h = Selector.handlers, selectorType, m; - var exclusions = new Selector(selector).findElements(root); - h.mark(exclusions); - for (var i = 0, results = [], node; node = nodes[i]; i++) - if (!node._countedByPrototype) results.push(node); - h.unmark(exclusions); - return results; - }, + positionedOffset = positionedOffset.wrap(function(proceed, element) { + element = $(element); + if (!element.parentNode) return new Element.Offset(0, 0); + var position = element.getStyle('position'); + if (position !== 'static') return proceed(element); - 'enabled': function(nodes, value, root) { - for (var i = 0, results = [], node; node = nodes[i]; i++) - if (!node.disabled && (!node.type || node.type !== 'hidden')) - results.push(node); - return results; - }, + var offsetParent = element.getOffsetParent(); + if (offsetParent && offsetParent.getStyle('position') === 'fixed') + hasLayout(offsetParent); - 'disabled': function(nodes, value, root) { - for (var i = 0, results = [], node; node = nodes[i]; i++) - if (node.disabled) results.push(node); - return results; - }, + element.setStyle({ position: 'relative' }); + var value = proceed(element); + element.setStyle({ position: position }); + return value; + }); + } else if (Prototype.Browser.Webkit) { + cumulativeOffset = function(element) { + element = $(element); + var valueT = 0, valueL = 0; + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + if (element.offsetParent == document.body) + if (Element.getStyle(element, 'position') == 'absolute') break; - 'checked': function(nodes, value, root) { - for (var i = 0, results = [], node; node = nodes[i]; i++) - if (node.checked) results.push(node); - return results; - } - }, + element = element.offsetParent; + } while (element); - operators: { - '=': function(nv, v) { return nv == v; }, - '!=': function(nv, v) { return nv != v; }, - '^=': function(nv, v) { return nv == v || nv && nv.startsWith(v); }, - '$=': function(nv, v) { return nv == v || nv && nv.endsWith(v); }, - '*=': function(nv, v) { return nv == v || nv && nv.include(v); }, - '~=': function(nv, v) { return (' ' + nv + ' ').include(' ' + v + ' '); }, - '|=': function(nv, v) { return ('-' + (nv || "").toUpperCase() + - '-').include('-' + (v || "").toUpperCase() + '-'); } - }, + return new Element.Offset(valueL, valueT); + }; + } + + + Element.addMethods({ + getLayout: getLayout, + measure: measure, + getDimensions: getDimensions, + getOffsetParent: getOffsetParent, + cumulativeOffset: cumulativeOffset, + positionedOffset: positionedOffset, + cumulativeScrollOffset: cumulativeScrollOffset, + viewportOffset: viewportOffset, + absolutize: absolutize, + relativize: relativize + }); + + function isBody(element) { + return element.nodeName.toUpperCase() === 'BODY'; + } + + function isHtml(element) { + return element.nodeName.toUpperCase() === 'HTML'; + } + + function isDocument(element) { + return element.nodeType === Node.DOCUMENT_NODE; + } + + function isDetached(element) { + return element !== document.body && + !Element.descendantOf(element, document.body); + } + + if ('getBoundingClientRect' in document.documentElement) { + Element.addMethods({ + viewportOffset: function(element) { + element = $(element); + if (isDetached(element)) return new Element.Offset(0, 0); - split: function(expression) { - var expressions = []; - expression.scan(/(([\w#:.~>+()\s-]+|\*|\[.*?\])+)\s*(,|$)/, function(m) { - expressions.push(m[1].strip()); + var rect = element.getBoundingClientRect(), + docEl = document.documentElement; + return new Element.Offset(rect.left - docEl.clientLeft, + rect.top - docEl.clientTop); + } }); - return expressions; - }, + } +})(); +window.$$ = function() { + var expression = $A(arguments).join(', '); + return Prototype.Selector.select(expression, document); +}; - matchElements: function(elements, expression) { - var matches = $$(expression), h = Selector.handlers; - h.mark(matches); - for (var i = 0, results = [], element; element = elements[i]; i++) - if (element._countedByPrototype) results.push(element); - h.unmark(matches); - return results; - }, +Prototype.Selector = (function() { - findElement: function(elements, expression, index) { - if (Object.isNumber(expression)) { - index = expression; expression = false; + function select() { + throw new Error('Method "Prototype.Selector.select" must be defined.'); + } + + function match() { + throw new Error('Method "Prototype.Selector.match" must be defined.'); + } + + function find(elements, expression, index) { + index = index || 0; + var match = Prototype.Selector.match, length = elements.length, matchIndex = 0, i; + + for (i = 0; i < length; i++) { + if (match(elements[i], expression) && index == matchIndex++) { + return Element.extend(elements[i]); + } } - return Selector.matchElements(elements, expression || '*')[index || 0]; - }, + } - findChildElements: function(element, expressions) { - expressions = Selector.split(expressions.join(',')); - var results = [], h = Selector.handlers; - for (var i = 0, l = expressions.length, selector; i < l; i++) { - selector = new Selector(expressions[i].strip()); - h.concat(results, selector.findElements(element)); + function extendElements(elements) { + for (var i = 0, length = elements.length; i < length; i++) { + Element.extend(elements[i]); } - return (l > 1) ? h.unique(results) : results; + return elements; } + + + var K = Prototype.K; + + return { + select: select, + match: match, + find: find, + extendElements: (Element.extend === K) ? K : extendElements, + extendElement: Element.extend + }; +})(); +/*! + * Sizzle CSS Selector Engine - v1.0 + * Copyright 2009, The Dojo Foundation + * Released under the MIT, BSD, and GPL Licenses. + * More information: http://sizzlejs.com/ + */ +(function(){ + +var chunker = /((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^[\]]*\]|['"][^'"]*['"]|[^[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g, + done = 0, + toString = Object.prototype.toString, + hasDuplicate = false, + baseHasDuplicate = true; + +[0, 0].sort(function(){ + baseHasDuplicate = false; + return 0; }); -if (Prototype.Browser.IE) { - Object.extend(Selector.handlers, { - concat: function(a, b) { - for (var i = 0, node; node = b[i]; i++) - if (node.tagName !== "!") a.push(node); - return a; - } - }); +var Sizzle = function(selector, context, results, seed) { + results = results || []; + var origContext = context = context || document; + + if ( context.nodeType !== 1 && context.nodeType !== 9 ) { + return []; + } + + if ( !selector || typeof selector !== "string" ) { + return results; + } + + var parts = [], m, set, checkSet, check, mode, extra, prune = true, contextXML = isXML(context), + soFar = selector; + + while ( (chunker.exec(""), m = chunker.exec(soFar)) !== null ) { + soFar = m[3]; + + parts.push( m[1] ); + + if ( m[2] ) { + extra = m[3]; + break; + } + } + + if ( parts.length > 1 && origPOS.exec( selector ) ) { + if ( parts.length === 2 && Expr.relative[ parts[0] ] ) { + set = posProcess( parts[0] + parts[1], context ); + } else { + set = Expr.relative[ parts[0] ] ? + [ context ] : + Sizzle( parts.shift(), context ); + + while ( parts.length ) { + selector = parts.shift(); + + if ( Expr.relative[ selector ] ) + selector += parts.shift(); + + set = posProcess( selector, set ); + } + } + } else { + if ( !seed && parts.length > 1 && context.nodeType === 9 && !contextXML && + Expr.match.ID.test(parts[0]) && !Expr.match.ID.test(parts[parts.length - 1]) ) { + var ret = Sizzle.find( parts.shift(), context, contextXML ); + context = ret.expr ? Sizzle.filter( ret.expr, ret.set )[0] : ret.set[0]; + } + + if ( context ) { + var ret = seed ? + { expr: parts.pop(), set: makeArray(seed) } : + Sizzle.find( parts.pop(), parts.length === 1 && (parts[0] === "~" || parts[0] === "+") && context.parentNode ? context.parentNode : context, contextXML ); + set = ret.expr ? Sizzle.filter( ret.expr, ret.set ) : ret.set; + + if ( parts.length > 0 ) { + checkSet = makeArray(set); + } else { + prune = false; + } + + while ( parts.length ) { + var cur = parts.pop(), pop = cur; + + if ( !Expr.relative[ cur ] ) { + cur = ""; + } else { + pop = parts.pop(); + } + + if ( pop == null ) { + pop = context; + } + + Expr.relative[ cur ]( checkSet, pop, contextXML ); + } + } else { + checkSet = parts = []; + } + } + + if ( !checkSet ) { + checkSet = set; + } + + if ( !checkSet ) { + throw "Syntax error, unrecognized expression: " + (cur || selector); + } + + if ( toString.call(checkSet) === "[object Array]" ) { + if ( !prune ) { + results.push.apply( results, checkSet ); + } else if ( context && context.nodeType === 1 ) { + for ( var i = 0; checkSet[i] != null; i++ ) { + if ( checkSet[i] && (checkSet[i] === true || checkSet[i].nodeType === 1 && contains(context, checkSet[i])) ) { + results.push( set[i] ); + } + } + } else { + for ( var i = 0; checkSet[i] != null; i++ ) { + if ( checkSet[i] && checkSet[i].nodeType === 1 ) { + results.push( set[i] ); + } + } + } + } else { + makeArray( checkSet, results ); + } + + if ( extra ) { + Sizzle( extra, origContext, results, seed ); + Sizzle.uniqueSort( results ); + } + + return results; +}; + +Sizzle.uniqueSort = function(results){ + if ( sortOrder ) { + hasDuplicate = baseHasDuplicate; + results.sort(sortOrder); + + if ( hasDuplicate ) { + for ( var i = 1; i < results.length; i++ ) { + if ( results[i] === results[i-1] ) { + results.splice(i--, 1); + } + } + } + } + + return results; +}; + +Sizzle.matches = function(expr, set){ + return Sizzle(expr, null, null, set); +}; + +Sizzle.find = function(expr, context, isXML){ + var set, match; + + if ( !expr ) { + return []; + } + + for ( var i = 0, l = Expr.order.length; i < l; i++ ) { + var type = Expr.order[i], match; + + if ( (match = Expr.leftMatch[ type ].exec( expr )) ) { + var left = match[1]; + match.splice(1,1); + + if ( left.substr( left.length - 1 ) !== "\\" ) { + match[1] = (match[1] || "").replace(/\\/g, ""); + set = Expr.find[ type ]( match, context, isXML ); + if ( set != null ) { + expr = expr.replace( Expr.match[ type ], "" ); + break; + } + } + } + } + + if ( !set ) { + set = context.getElementsByTagName("*"); + } + + return {set: set, expr: expr}; +}; + +Sizzle.filter = function(expr, set, inplace, not){ + var old = expr, result = [], curLoop = set, match, anyFound, + isXMLFilter = set && set[0] && isXML(set[0]); + + while ( expr && set.length ) { + for ( var type in Expr.filter ) { + if ( (match = Expr.match[ type ].exec( expr )) != null ) { + var filter = Expr.filter[ type ], found, item; + anyFound = false; + + if ( curLoop == result ) { + result = []; + } + + if ( Expr.preFilter[ type ] ) { + match = Expr.preFilter[ type ]( match, curLoop, inplace, result, not, isXMLFilter ); + + if ( !match ) { + anyFound = found = true; + } else if ( match === true ) { + continue; + } + } + + if ( match ) { + for ( var i = 0; (item = curLoop[i]) != null; i++ ) { + if ( item ) { + found = filter( item, match, i, curLoop ); + var pass = not ^ !!found; + + if ( inplace && found != null ) { + if ( pass ) { + anyFound = true; + } else { + curLoop[i] = false; + } + } else if ( pass ) { + result.push( item ); + anyFound = true; + } + } + } + } + + if ( found !== undefined ) { + if ( !inplace ) { + curLoop = result; + } + + expr = expr.replace( Expr.match[ type ], "" ); + + if ( !anyFound ) { + return []; + } + + break; + } + } + } + + if ( expr == old ) { + if ( anyFound == null ) { + throw "Syntax error, unrecognized expression: " + expr; + } else { + break; + } + } + + old = expr; + } + + return curLoop; +}; + +var Expr = Sizzle.selectors = { + order: [ "ID", "NAME", "TAG" ], + match: { + ID: /#((?:[\w\u00c0-\uFFFF-]|\\.)+)/, + CLASS: /\.((?:[\w\u00c0-\uFFFF-]|\\.)+)/, + NAME: /\[name=['"]*((?:[\w\u00c0-\uFFFF-]|\\.)+)['"]*\]/, + ATTR: /\[\s*((?:[\w\u00c0-\uFFFF-]|\\.)+)\s*(?:(\S?=)\s*(['"]*)(.*?)\3|)\s*\]/, + TAG: /^((?:[\w\u00c0-\uFFFF\*-]|\\.)+)/, + CHILD: /:(only|nth|last|first)-child(?:\((even|odd|[\dn+-]*)\))?/, + POS: /:(nth|eq|gt|lt|first|last|even|odd)(?:\((\d*)\))?(?=[^-]|$)/, + PSEUDO: /:((?:[\w\u00c0-\uFFFF-]|\\.)+)(?:\((['"]*)((?:\([^\)]+\)|[^\2\(\)]*)+)\2\))?/ + }, + leftMatch: {}, + attrMap: { + "class": "className", + "for": "htmlFor" + }, + attrHandle: { + href: function(elem){ + return elem.getAttribute("href"); + } + }, + relative: { + "+": function(checkSet, part, isXML){ + var isPartStr = typeof part === "string", + isTag = isPartStr && !/\W/.test(part), + isPartStrNotTag = isPartStr && !isTag; + + if ( isTag && !isXML ) { + part = part.toUpperCase(); + } + + for ( var i = 0, l = checkSet.length, elem; i < l; i++ ) { + if ( (elem = checkSet[i]) ) { + while ( (elem = elem.previousSibling) && elem.nodeType !== 1 ) {} + + checkSet[i] = isPartStrNotTag || elem && elem.nodeName === part ? + elem || false : + elem === part; + } + } + + if ( isPartStrNotTag ) { + Sizzle.filter( part, checkSet, true ); + } + }, + ">": function(checkSet, part, isXML){ + var isPartStr = typeof part === "string"; + + if ( isPartStr && !/\W/.test(part) ) { + part = isXML ? part : part.toUpperCase(); + + for ( var i = 0, l = checkSet.length; i < l; i++ ) { + var elem = checkSet[i]; + if ( elem ) { + var parent = elem.parentNode; + checkSet[i] = parent.nodeName === part ? parent : false; + } + } + } else { + for ( var i = 0, l = checkSet.length; i < l; i++ ) { + var elem = checkSet[i]; + if ( elem ) { + checkSet[i] = isPartStr ? + elem.parentNode : + elem.parentNode === part; + } + } + + if ( isPartStr ) { + Sizzle.filter( part, checkSet, true ); + } + } + }, + "": function(checkSet, part, isXML){ + var doneName = done++, checkFn = dirCheck; + + if ( !/\W/.test(part) ) { + var nodeCheck = part = isXML ? part : part.toUpperCase(); + checkFn = dirNodeCheck; + } + + checkFn("parentNode", part, doneName, checkSet, nodeCheck, isXML); + }, + "~": function(checkSet, part, isXML){ + var doneName = done++, checkFn = dirCheck; + + if ( typeof part === "string" && !/\W/.test(part) ) { + var nodeCheck = part = isXML ? part : part.toUpperCase(); + checkFn = dirNodeCheck; + } + + checkFn("previousSibling", part, doneName, checkSet, nodeCheck, isXML); + } + }, + find: { + ID: function(match, context, isXML){ + if ( typeof context.getElementById !== "undefined" && !isXML ) { + var m = context.getElementById(match[1]); + return m ? [m] : []; + } + }, + NAME: function(match, context, isXML){ + if ( typeof context.getElementsByName !== "undefined" ) { + var ret = [], results = context.getElementsByName(match[1]); + + for ( var i = 0, l = results.length; i < l; i++ ) { + if ( results[i].getAttribute("name") === match[1] ) { + ret.push( results[i] ); + } + } + + return ret.length === 0 ? null : ret; + } + }, + TAG: function(match, context){ + return context.getElementsByTagName(match[1]); + } + }, + preFilter: { + CLASS: function(match, curLoop, inplace, result, not, isXML){ + match = " " + match[1].replace(/\\/g, "") + " "; + + if ( isXML ) { + return match; + } + + for ( var i = 0, elem; (elem = curLoop[i]) != null; i++ ) { + if ( elem ) { + if ( not ^ (elem.className && (" " + elem.className + " ").indexOf(match) >= 0) ) { + if ( !inplace ) + result.push( elem ); + } else if ( inplace ) { + curLoop[i] = false; + } + } + } + + return false; + }, + ID: function(match){ + return match[1].replace(/\\/g, ""); + }, + TAG: function(match, curLoop){ + for ( var i = 0; curLoop[i] === false; i++ ){} + return curLoop[i] && isXML(curLoop[i]) ? match[1] : match[1].toUpperCase(); + }, + CHILD: function(match){ + if ( match[1] == "nth" ) { + var test = /(-?)(\d*)n((?:\+|-)?\d*)/.exec( + match[2] == "even" && "2n" || match[2] == "odd" && "2n+1" || + !/\D/.test( match[2] ) && "0n+" + match[2] || match[2]); + + match[2] = (test[1] + (test[2] || 1)) - 0; + match[3] = test[3] - 0; + } + + match[0] = done++; + + return match; + }, + ATTR: function(match, curLoop, inplace, result, not, isXML){ + var name = match[1].replace(/\\/g, ""); + + if ( !isXML && Expr.attrMap[name] ) { + match[1] = Expr.attrMap[name]; + } + + if ( match[2] === "~=" ) { + match[4] = " " + match[4] + " "; + } + + return match; + }, + PSEUDO: function(match, curLoop, inplace, result, not){ + if ( match[1] === "not" ) { + if ( ( chunker.exec(match[3]) || "" ).length > 1 || /^\w/.test(match[3]) ) { + match[3] = Sizzle(match[3], null, null, curLoop); + } else { + var ret = Sizzle.filter(match[3], curLoop, inplace, true ^ not); + if ( !inplace ) { + result.push.apply( result, ret ); + } + return false; + } + } else if ( Expr.match.POS.test( match[0] ) || Expr.match.CHILD.test( match[0] ) ) { + return true; + } + + return match; + }, + POS: function(match){ + match.unshift( true ); + return match; + } + }, + filters: { + enabled: function(elem){ + return elem.disabled === false && elem.type !== "hidden"; + }, + disabled: function(elem){ + return elem.disabled === true; + }, + checked: function(elem){ + return elem.checked === true; + }, + selected: function(elem){ + elem.parentNode.selectedIndex; + return elem.selected === true; + }, + parent: function(elem){ + return !!elem.firstChild; + }, + empty: function(elem){ + return !elem.firstChild; + }, + has: function(elem, i, match){ + return !!Sizzle( match[3], elem ).length; + }, + header: function(elem){ + return /h\d/i.test( elem.nodeName ); + }, + text: function(elem){ + return "text" === elem.type; + }, + radio: function(elem){ + return "radio" === elem.type; + }, + checkbox: function(elem){ + return "checkbox" === elem.type; + }, + file: function(elem){ + return "file" === elem.type; + }, + password: function(elem){ + return "password" === elem.type; + }, + submit: function(elem){ + return "submit" === elem.type; + }, + image: function(elem){ + return "image" === elem.type; + }, + reset: function(elem){ + return "reset" === elem.type; + }, + button: function(elem){ + return "button" === elem.type || elem.nodeName.toUpperCase() === "BUTTON"; + }, + input: function(elem){ + return /input|select|textarea|button/i.test(elem.nodeName); + } + }, + setFilters: { + first: function(elem, i){ + return i === 0; + }, + last: function(elem, i, match, array){ + return i === array.length - 1; + }, + even: function(elem, i){ + return i % 2 === 0; + }, + odd: function(elem, i){ + return i % 2 === 1; + }, + lt: function(elem, i, match){ + return i < match[3] - 0; + }, + gt: function(elem, i, match){ + return i > match[3] - 0; + }, + nth: function(elem, i, match){ + return match[3] - 0 == i; + }, + eq: function(elem, i, match){ + return match[3] - 0 == i; + } + }, + filter: { + PSEUDO: function(elem, match, i, array){ + var name = match[1], filter = Expr.filters[ name ]; + + if ( filter ) { + return filter( elem, i, match, array ); + } else if ( name === "contains" ) { + return (elem.textContent || elem.innerText || "").indexOf(match[3]) >= 0; + } else if ( name === "not" ) { + var not = match[3]; + + for ( var i = 0, l = not.length; i < l; i++ ) { + if ( not[i] === elem ) { + return false; + } + } + + return true; + } + }, + CHILD: function(elem, match){ + var type = match[1], node = elem; + switch (type) { + case 'only': + case 'first': + while ( (node = node.previousSibling) ) { + if ( node.nodeType === 1 ) return false; + } + if ( type == 'first') return true; + node = elem; + case 'last': + while ( (node = node.nextSibling) ) { + if ( node.nodeType === 1 ) return false; + } + return true; + case 'nth': + var first = match[2], last = match[3]; + + if ( first == 1 && last == 0 ) { + return true; + } + + var doneName = match[0], + parent = elem.parentNode; + + if ( parent && (parent.sizcache !== doneName || !elem.nodeIndex) ) { + var count = 0; + for ( node = parent.firstChild; node; node = node.nextSibling ) { + if ( node.nodeType === 1 ) { + node.nodeIndex = ++count; + } + } + parent.sizcache = doneName; + } + + var diff = elem.nodeIndex - last; + if ( first == 0 ) { + return diff == 0; + } else { + return ( diff % first == 0 && diff / first >= 0 ); + } + } + }, + ID: function(elem, match){ + return elem.nodeType === 1 && elem.getAttribute("id") === match; + }, + TAG: function(elem, match){ + return (match === "*" && elem.nodeType === 1) || elem.nodeName === match; + }, + CLASS: function(elem, match){ + return (" " + (elem.className || elem.getAttribute("class")) + " ") + .indexOf( match ) > -1; + }, + ATTR: function(elem, match){ + var name = match[1], + result = Expr.attrHandle[ name ] ? + Expr.attrHandle[ name ]( elem ) : + elem[ name ] != null ? + elem[ name ] : + elem.getAttribute( name ), + value = result + "", + type = match[2], + check = match[4]; + + return result == null ? + type === "!=" : + type === "=" ? + value === check : + type === "*=" ? + value.indexOf(check) >= 0 : + type === "~=" ? + (" " + value + " ").indexOf(check) >= 0 : + !check ? + value && result !== false : + type === "!=" ? + value != check : + type === "^=" ? + value.indexOf(check) === 0 : + type === "$=" ? + value.substr(value.length - check.length) === check : + type === "|=" ? + value === check || value.substr(0, check.length + 1) === check + "-" : + false; + }, + POS: function(elem, match, i, array){ + var name = match[2], filter = Expr.setFilters[ name ]; + + if ( filter ) { + return filter( elem, i, match, array ); + } + } + } +}; + +var origPOS = Expr.match.POS; + +for ( var type in Expr.match ) { + Expr.match[ type ] = new RegExp( Expr.match[ type ].source + /(?![^\[]*\])(?![^\(]*\))/.source ); + Expr.leftMatch[ type ] = new RegExp( /(^(?:.|\r|\n)*?)/.source + Expr.match[ type ].source ); +} + +var makeArray = function(array, results) { + array = Array.prototype.slice.call( array, 0 ); + + if ( results ) { + results.push.apply( results, array ); + return results; + } + + return array; +}; + +try { + Array.prototype.slice.call( document.documentElement.childNodes, 0 ); + +} catch(e){ + makeArray = function(array, results) { + var ret = results || []; + + if ( toString.call(array) === "[object Array]" ) { + Array.prototype.push.apply( ret, array ); + } else { + if ( typeof array.length === "number" ) { + for ( var i = 0, l = array.length; i < l; i++ ) { + ret.push( array[i] ); + } + } else { + for ( var i = 0; array[i]; i++ ) { + ret.push( array[i] ); + } + } + } + + return ret; + }; +} + +var sortOrder; + +if ( document.documentElement.compareDocumentPosition ) { + sortOrder = function( a, b ) { + if ( !a.compareDocumentPosition || !b.compareDocumentPosition ) { + if ( a == b ) { + hasDuplicate = true; + } + return 0; + } + + var ret = a.compareDocumentPosition(b) & 4 ? -1 : a === b ? 0 : 1; + if ( ret === 0 ) { + hasDuplicate = true; + } + return ret; + }; +} else if ( "sourceIndex" in document.documentElement ) { + sortOrder = function( a, b ) { + if ( !a.sourceIndex || !b.sourceIndex ) { + if ( a == b ) { + hasDuplicate = true; + } + return 0; + } + + var ret = a.sourceIndex - b.sourceIndex; + if ( ret === 0 ) { + hasDuplicate = true; + } + return ret; + }; +} else if ( document.createRange ) { + sortOrder = function( a, b ) { + if ( !a.ownerDocument || !b.ownerDocument ) { + if ( a == b ) { + hasDuplicate = true; + } + return 0; + } + + var aRange = a.ownerDocument.createRange(), bRange = b.ownerDocument.createRange(); + aRange.setStart(a, 0); + aRange.setEnd(a, 0); + bRange.setStart(b, 0); + bRange.setEnd(b, 0); + var ret = aRange.compareBoundaryPoints(Range.START_TO_END, bRange); + if ( ret === 0 ) { + hasDuplicate = true; + } + return ret; + }; +} + +(function(){ + var form = document.createElement("div"), + id = "script" + (new Date).getTime(); + form.innerHTML = "<a name='" + id + "'/>"; + + var root = document.documentElement; + root.insertBefore( form, root.firstChild ); + + if ( !!document.getElementById( id ) ) { + Expr.find.ID = function(match, context, isXML){ + if ( typeof context.getElementById !== "undefined" && !isXML ) { + var m = context.getElementById(match[1]); + return m ? m.id === match[1] || typeof m.getAttributeNode !== "undefined" && m.getAttributeNode("id").nodeValue === match[1] ? [m] : undefined : []; + } + }; + + Expr.filter.ID = function(elem, match){ + var node = typeof elem.getAttributeNode !== "undefined" && elem.getAttributeNode("id"); + return elem.nodeType === 1 && node && node.nodeValue === match; + }; + } + + root.removeChild( form ); + root = form = null; // release memory in IE +})(); + +(function(){ + + var div = document.createElement("div"); + div.appendChild( document.createComment("") ); + + if ( div.getElementsByTagName("*").length > 0 ) { + Expr.find.TAG = function(match, context){ + var results = context.getElementsByTagName(match[1]); + + if ( match[1] === "*" ) { + var tmp = []; + + for ( var i = 0; results[i]; i++ ) { + if ( results[i].nodeType === 1 ) { + tmp.push( results[i] ); + } + } + + results = tmp; + } + + return results; + }; + } + + div.innerHTML = "<a href='#'></a>"; + if ( div.firstChild && typeof div.firstChild.getAttribute !== "undefined" && + div.firstChild.getAttribute("href") !== "#" ) { + Expr.attrHandle.href = function(elem){ + return elem.getAttribute("href", 2); + }; + } + + div = null; // release memory in IE +})(); + +if ( document.querySelectorAll ) (function(){ + var oldSizzle = Sizzle, div = document.createElement("div"); + div.innerHTML = "<p class='TEST'></p>"; + + if ( div.querySelectorAll && div.querySelectorAll(".TEST").length === 0 ) { + return; + } + + Sizzle = function(query, context, extra, seed){ + context = context || document; + + if ( !seed && context.nodeType === 9 && !isXML(context) ) { + try { + return makeArray( context.querySelectorAll(query), extra ); + } catch(e){} + } + + return oldSizzle(query, context, extra, seed); + }; + + for ( var prop in oldSizzle ) { + Sizzle[ prop ] = oldSizzle[ prop ]; + } + + div = null; // release memory in IE +})(); + +if ( document.getElementsByClassName && document.documentElement.getElementsByClassName ) (function(){ + var div = document.createElement("div"); + div.innerHTML = "<div class='test e'></div><div class='test'></div>"; + + if ( div.getElementsByClassName("e").length === 0 ) + return; + + div.lastChild.className = "e"; + + if ( div.getElementsByClassName("e").length === 1 ) + return; + + Expr.order.splice(1, 0, "CLASS"); + Expr.find.CLASS = function(match, context, isXML) { + if ( typeof context.getElementsByClassName !== "undefined" && !isXML ) { + return context.getElementsByClassName(match[1]); + } + }; + + div = null; // release memory in IE +})(); + +function dirNodeCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) { + var sibDir = dir == "previousSibling" && !isXML; + for ( var i = 0, l = checkSet.length; i < l; i++ ) { + var elem = checkSet[i]; + if ( elem ) { + if ( sibDir && elem.nodeType === 1 ){ + elem.sizcache = doneName; + elem.sizset = i; + } + elem = elem[dir]; + var match = false; + + while ( elem ) { + if ( elem.sizcache === doneName ) { + match = checkSet[elem.sizset]; + break; + } + + if ( elem.nodeType === 1 && !isXML ){ + elem.sizcache = doneName; + elem.sizset = i; + } + + if ( elem.nodeName === cur ) { + match = elem; + break; + } + + elem = elem[dir]; + } + + checkSet[i] = match; + } + } } -function $$() { - return Selector.findChildElements(document, $A(arguments)); +function dirCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) { + var sibDir = dir == "previousSibling" && !isXML; + for ( var i = 0, l = checkSet.length; i < l; i++ ) { + var elem = checkSet[i]; + if ( elem ) { + if ( sibDir && elem.nodeType === 1 ) { + elem.sizcache = doneName; + elem.sizset = i; + } + elem = elem[dir]; + var match = false; + + while ( elem ) { + if ( elem.sizcache === doneName ) { + match = checkSet[elem.sizset]; + break; + } + + if ( elem.nodeType === 1 ) { + if ( !isXML ) { + elem.sizcache = doneName; + elem.sizset = i; + } + if ( typeof cur !== "string" ) { + if ( elem === cur ) { + match = true; + break; + } + + } else if ( Sizzle.filter( cur, [elem] ).length > 0 ) { + match = elem; + break; + } + } + + elem = elem[dir]; + } + + checkSet[i] = match; + } + } } +var contains = document.compareDocumentPosition ? function(a, b){ + return a.compareDocumentPosition(b) & 16; +} : function(a, b){ + return a !== b && (a.contains ? a.contains(b) : true); +}; + +var isXML = function(elem){ + return elem.nodeType === 9 && elem.documentElement.nodeName !== "HTML" || + !!elem.ownerDocument && elem.ownerDocument.documentElement.nodeName !== "HTML"; +}; + +var posProcess = function(selector, context){ + var tmpSet = [], later = "", match, + root = context.nodeType ? [context] : context; + + while ( (match = Expr.match.PSEUDO.exec( selector )) ) { + later += match[0]; + selector = selector.replace( Expr.match.PSEUDO, "" ); + } + + selector = Expr.relative[selector] ? selector + "*" : selector; + + for ( var i = 0, l = root.length; i < l; i++ ) { + Sizzle( selector, root[i], tmpSet ); + } + + return Sizzle.filter( later, tmpSet ); +}; + + +window.Sizzle = Sizzle; + +})(); + +Prototype._original_property = window.Sizzle; + +;(function(engine) { + var extendElements = Prototype.Selector.extendElements; + + function select(selector, scope) { + return extendElements(engine(selector, scope || document)); + } + + function match(element, selector) { + return engine.matches(selector, [element]).length == 1; + } + + Prototype.Selector.engine = engine; + Prototype.Selector.select = select; + Prototype.Selector.match = match; +})(Sizzle); + +window.Sizzle = Prototype._original_property; +delete Prototype._original_property; + var Form = { reset: function(form) { form = $(form); @@ -3924,24 +4969,34 @@ var Form = { serializeElements: function(elements, options) { if (typeof options != 'object') options = { hash: !!options }; else if (Object.isUndefined(options.hash)) options.hash = true; - var key, value, submitted = false, submit = options.submit; + var key, value, submitted = false, submit = options.submit, accumulator, initial; + + if (options.hash) { + initial = {}; + accumulator = function(result, key, value) { + if (key in result) { + if (!Object.isArray(result[key])) result[key] = [result[key]]; + result[key].push(value); + } else result[key] = value; + return result; + }; + } else { + initial = ''; + accumulator = function(result, key, value) { + return result + (result ? '&' : '') + encodeURIComponent(key) + '=' + encodeURIComponent(value); + } + } - var data = elements.inject({ }, function(result, element) { + return elements.inject(initial, function(result, element) { if (!element.disabled && element.name) { key = element.name; value = $(element).getValue(); if (value != null && element.type != 'file' && (element.type != 'submit' || (!submitted && submit !== false && (!submit || key == submit) && (submitted = true)))) { - if (key in result) { - if (!Object.isArray(result[key])) result[key] = [result[key]]; - result[key].push(value); - } - else result[key] = value; + result = accumulator(result, key, value); } } return result; }); - - return options.hash ? data : Object.toQueryString(data); } }; @@ -4008,7 +5063,8 @@ Form.Methods = { focusFirstElement: function(form) { form = $(form); - form.findFirstElement().activate(); + var element = form.findFirstElement(); + if (element) element.activate(); return form; }, @@ -4115,67 +5171,77 @@ var $F = Form.Element.Methods.getValue; /*--------------------------------------------------------------------------*/ -Form.Element.Serializers = { - input: function(element, value) { +Form.Element.Serializers = (function() { + function input(element, value) { switch (element.type.toLowerCase()) { case 'checkbox': case 'radio': - return Form.Element.Serializers.inputSelector(element, value); + return inputSelector(element, value); default: - return Form.Element.Serializers.textarea(element, value); + return valueSelector(element, value); } - }, + } - inputSelector: function(element, value) { - if (Object.isUndefined(value)) return element.checked ? element.value : null; + function inputSelector(element, value) { + if (Object.isUndefined(value)) + return element.checked ? element.value : null; else element.checked = !!value; - }, + } - textarea: function(element, value) { + function valueSelector(element, value) { if (Object.isUndefined(value)) return element.value; else element.value = value; - }, + } - select: function(element, value) { + function select(element, value) { if (Object.isUndefined(value)) - return this[element.type == 'select-one' ? - 'selectOne' : 'selectMany'](element); - else { - var opt, currentValue, single = !Object.isArray(value); - for (var i = 0, length = element.length; i < length; i++) { - opt = element.options[i]; - currentValue = this.optionValue(opt); - if (single) { - if (currentValue == value) { - opt.selected = true; - return; - } + return (element.type === 'select-one' ? selectOne : selectMany)(element); + + var opt, currentValue, single = !Object.isArray(value); + for (var i = 0, length = element.length; i < length; i++) { + opt = element.options[i]; + currentValue = this.optionValue(opt); + if (single) { + if (currentValue == value) { + opt.selected = true; + return; } - else opt.selected = value.include(currentValue); } + else opt.selected = value.include(currentValue); } - }, + } - selectOne: function(element) { + function selectOne(element) { var index = element.selectedIndex; - return index >= 0 ? this.optionValue(element.options[index]) : null; - }, + return index >= 0 ? optionValue(element.options[index]) : null; + } - selectMany: function(element) { + function selectMany(element) { var values, length = element.length; if (!length) return null; for (var i = 0, values = []; i < length; i++) { var opt = element.options[i]; - if (opt.selected) values.push(this.optionValue(opt)); + if (opt.selected) values.push(optionValue(opt)); } return values; - }, + } - optionValue: function(opt) { - return Element.extend(opt).hasAttribute('value') ? opt.value : opt.text; + function optionValue(opt) { + return Element.hasAttribute(opt, 'value') ? opt.value : opt.text; } -}; + + return { + input: input, + inputSelector: inputSelector, + textarea: valueSelector, + select: select, + selectOne: selectOne, + selectMany: selectMany, + optionValue: optionValue, + button: valueSelector + }; +})(); /*--------------------------------------------------------------------------*/ @@ -4286,24 +5352,53 @@ Form.EventObserver = Class.create(Abstract.EventObserver, { var MOUSEENTER_MOUSELEAVE_EVENTS_SUPPORTED = 'onmouseenter' in docEl && 'onmouseleave' in docEl; + + + var isIELegacyEvent = function(event) { return false; }; + + if (window.attachEvent) { + if (window.addEventListener) { + isIELegacyEvent = function(event) { + return !(event instanceof window.Event); + }; + } else { + isIELegacyEvent = function(event) { return true; }; + } + } + var _isButton; - if (Prototype.Browser.IE) { - var buttonMap = { 0: 1, 1: 4, 2: 2 }; - _isButton = function(event, code) { - return event.button === buttonMap[code]; - }; - } else if (Prototype.Browser.WebKit) { - _isButton = function(event, code) { - switch (code) { - case 0: return event.which == 1 && !event.metaKey; - case 1: return event.which == 1 && event.metaKey; - default: return false; + + function _isButtonForDOMEvents(event, code) { + return event.which ? (event.which === code + 1) : (event.button === code); + } + + var legacyButtonMap = { 0: 1, 1: 4, 2: 2 }; + function _isButtonForLegacyEvents(event, code) { + return event.button === legacyButtonMap[code]; + } + + function _isButtonForWebKit(event, code) { + switch (code) { + case 0: return event.which == 1 && !event.metaKey; + case 1: return event.which == 2 || (event.which == 1 && event.metaKey); + case 2: return event.which == 3; + default: return false; + } + } + + if (window.attachEvent) { + if (!window.addEventListener) { + _isButton = _isButtonForLegacyEvents; + } else { + _isButton = function(event, code) { + return isIELegacyEvent(event) ? _isButtonForLegacyEvents(event, code) : + _isButtonForDOMEvents(event, code); } - }; + } + } else if (Prototype.Browser.WebKit) { + _isButton = _isButtonForWebKit; } else { - _isButton = function(event, code) { - return event.which ? (event.which === code + 1) : (event.button === code); - }; + _isButton = _isButtonForDOMEvents; } function isLeftClick(event) { return _isButton(event, 0) } @@ -4333,9 +5428,14 @@ Form.EventObserver = Class.create(Abstract.EventObserver, { function findElement(event, expression) { var element = Event.element(event); + if (!expression) return element; - var elements = [element].concat(element.ancestors()); - return Selector.findElement(elements, expression, 0); + while (element) { + if (Object.isElement(element) && Prototype.Selector.match(element, expression)) { + return Element.extend(element); + } + element = element.parentNode; + } } function pointer(event) { @@ -4369,49 +5469,59 @@ Form.EventObserver = Class.create(Abstract.EventObserver, { event.stopped = true; } + Event.Methods = { - isLeftClick: isLeftClick, + isLeftClick: isLeftClick, isMiddleClick: isMiddleClick, - isRightClick: isRightClick, + isRightClick: isRightClick, - element: element, + element: element, findElement: findElement, - pointer: pointer, + pointer: pointer, pointerX: pointerX, pointerY: pointerY, stop: stop }; - var methods = Object.keys(Event.Methods).inject({ }, function(m, name) { m[name] = Event.Methods[name].methodize(); return m; }); - if (Prototype.Browser.IE) { + if (window.attachEvent) { function _relatedTarget(event) { var element; switch (event.type) { - case 'mouseover': element = event.fromElement; break; - case 'mouseout': element = event.toElement; break; - default: return null; + case 'mouseover': + case 'mouseenter': + element = event.fromElement; + break; + case 'mouseout': + case 'mouseleave': + element = event.toElement; + break; + default: + return null; } return Element.extend(element); } - Object.extend(methods, { + var additionalMethods = { stopPropagation: function() { this.cancelBubble = true }, preventDefault: function() { this.returnValue = false }, inspect: function() { return '[object Event]' } - }); + }; Event.extend = function(event, element) { if (!event) return false; - if (event._extendedByPrototype) return event; + if (!isIELegacyEvent(event)) return event; + + if (event._extendedByPrototype) return event; event._extendedByPrototype = Prototype.emptyFunction; + var pointer = Event.pointer(event); Object.extend(event, { @@ -4421,12 +5531,18 @@ Form.EventObserver = Class.create(Abstract.EventObserver, { pageY: pointer.y }); - return Object.extend(event, methods); + Object.extend(event, methods); + Object.extend(event, additionalMethods); + + return event; }; } else { + Event.extend = Prototype.K; + } + + if (window.addEventListener) { Event.prototype = window.Event.prototype || document.createEvent('HTMLEvents').__proto__; Object.extend(Event.prototype, methods); - Event.extend = Prototype.K; } function _createResponder(element, eventName, handler) { @@ -4504,12 +5620,12 @@ Form.EventObserver = Class.create(Abstract.EventObserver, { window.addEventListener('unload', Prototype.emptyFunction, false); - var _getDOMEventName = Prototype.K; + var _getDOMEventName = Prototype.K, + translations = { mouseenter: "mouseover", mouseleave: "mouseout" }; if (!MOUSEENTER_MOUSELEAVE_EVENTS_SUPPORTED) { _getDOMEventName = function(eventName) { - var translations = { mouseenter: "mouseover", mouseleave: "mouseout" }; - return eventName in translations ? translations[eventName] : eventName; + return (translations[eventName] || eventName); }; } @@ -4525,7 +5641,7 @@ Form.EventObserver = Class.create(Abstract.EventObserver, { element.addEventListener("dataavailable", responder, false); else { element.attachEvent("ondataavailable", responder); - element.attachEvent("onfilterchange", responder); + element.attachEvent("onlosecapture", responder); } } else { var actualEventName = _getDOMEventName(eventName); @@ -4543,46 +5659,44 @@ Form.EventObserver = Class.create(Abstract.EventObserver, { element = $(element); var registry = Element.retrieve(element, 'prototype_event_registry'); + if (!registry) return element; - if (Object.isUndefined(registry)) return element; - - if (eventName && !handler) { - var responders = registry.get(eventName); - - if (Object.isUndefined(responders)) return element; - - responders.each( function(r) { - Element.stopObserving(element, eventName, r.handler); - }); - return element; - } else if (!eventName) { + if (!eventName) { registry.each( function(pair) { - var eventName = pair.key, responders = pair.value; - - responders.each( function(r) { - Element.stopObserving(element, eventName, r.handler); - }); + var eventName = pair.key; + stopObserving(element, eventName); }); return element; } var responders = registry.get(eventName); + if (!responders) return element; - if (!responders) return; + if (!handler) { + responders.each(function(r) { + stopObserving(element, eventName, r.handler); + }); + return element; + } - var responder = responders.find( function(r) { return r.handler === handler; }); + var i = responders.length, responder; + while (i--) { + if (responders[i].handler === handler) { + responder = responders[i]; + break; + } + } if (!responder) return element; - var actualEventName = _getDOMEventName(eventName); - if (eventName.include(':')) { if (element.removeEventListener) element.removeEventListener("dataavailable", responder, false); else { element.detachEvent("ondataavailable", responder); - element.detachEvent("onfilterchange", responder); + element.detachEvent("onlosecapture", responder); } } else { + var actualEventName = _getDOMEventName(eventName); if (element.removeEventListener) element.removeEventListener(actualEventName, responder, false); else @@ -4606,10 +5720,10 @@ Form.EventObserver = Class.create(Abstract.EventObserver, { var event; if (document.createEvent) { event = document.createEvent('HTMLEvents'); - event.initEvent('dataavailable', true, true); + event.initEvent('dataavailable', bubble, true); } else { event = document.createEventObject(); - event.eventType = bubble ? 'ondataavailable' : 'onfilterchange'; + event.eventType = bubble ? 'ondataavailable' : 'onlosecapture'; } event.eventName = eventName; @@ -4623,13 +5737,47 @@ Form.EventObserver = Class.create(Abstract.EventObserver, { return Event.extend(event); } + Event.Handler = Class.create({ + initialize: function(element, eventName, selector, callback) { + this.element = $(element); + this.eventName = eventName; + this.selector = selector; + this.callback = callback; + this.handler = this.handleEvent.bind(this); + }, + + start: function() { + Event.observe(this.element, this.eventName, this.handler); + return this; + }, + + stop: function() { + Event.stopObserving(this.element, this.eventName, this.handler); + return this; + }, + + handleEvent: function(event) { + var element = Event.findElement(event, this.selector); + if (element) this.callback.call(this.element, event, element); + } + }); + + function on(element, eventName, selector, callback) { + element = $(element); + if (Object.isFunction(selector) && Object.isUndefined(callback)) { + callback = selector, selector = null; + } + + return new Event.Handler(element, eventName, selector, callback).start(); + } Object.extend(Event, Event.Methods); Object.extend(Event, { fire: fire, observe: observe, - stopObserving: stopObserving + stopObserving: stopObserving, + on: on }); Element.addMethods({ @@ -4637,7 +5785,9 @@ Form.EventObserver = Class.create(Abstract.EventObserver, { observe: observe, - stopObserving: stopObserving + stopObserving: stopObserving, + + on: on }); Object.extend(document, { @@ -4647,6 +5797,8 @@ Form.EventObserver = Class.create(Abstract.EventObserver, { stopObserving: stopObserving.methodize(), + on: on.methodize(), + loaded: false }); @@ -4694,8 +5846,8 @@ Form.EventObserver = Class.create(Abstract.EventObserver, { Event.observe(window, 'load', fireContentLoadedEvent); })(); -Element.addMethods(); +Element.addMethods(); /*------------------------------- DEPRECATED -------------------------------*/ Hash.toQueryString = Object.toQueryString; @@ -4871,4 +6023,59 @@ Element.ClassNames.prototype = { Object.extend(Element.ClassNames.prototype, Enumerable); -/*--------------------------------------------------------------------------*/
\ No newline at end of file +/*--------------------------------------------------------------------------*/ + +(function() { + window.Selector = Class.create({ + initialize: function(expression) { + this.expression = expression.strip(); + }, + + findElements: function(rootElement) { + return Prototype.Selector.select(this.expression, rootElement); + }, + + match: function(element) { + return Prototype.Selector.match(element, this.expression); + }, + + toString: function() { + return this.expression; + }, + + inspect: function() { + return "#<Selector: " + this.expression + ">"; + } + }); + + Object.extend(Selector, { + matchElements: function(elements, expression) { + var match = Prototype.Selector.match, + results = []; + + for (var i = 0, length = elements.length; i < length; i++) { + var element = elements[i]; + if (match(element, expression)) { + results.push(Element.extend(element)); + } + } + return results; + }, + + findElement: function(elements, expression, index) { + index = index || 0; + var matchIndex = 0, element; + for (var i = 0, length = elements.length; i < length; i++) { + element = elements[i]; + if (Prototype.Selector.match(element, expression) && index === matchIndex++) { + return Element.extend(element); + } + } + }, + + findChildElements: function(element, expressions) { + var selector = expressions.toArray().join(', '); + return Prototype.Selector.select(selector, element || document); + } + }); +})(); diff --git a/sonar-server/src/main/webapp/javascripts/protovis-msie-shim.js b/sonar-server/src/main/webapp/javascripts/protovis-msie-shim.js new file mode 100644 index 00000000000..d89ce57b190 --- /dev/null +++ b/sonar-server/src/main/webapp/javascripts/protovis-msie-shim.js @@ -0,0 +1,6 @@ +/* + * Protovis MSIE VML shim + * (c) 2011 DataMarket (datamarket@datamarket.com) + * + */ +pv.have_SVG=!!document.createElementNS&&!!document.createElementNS("http://www.w3.org/2000/svg","svg").createSVGRect,pv.have_VML=function(a,b,c){b=a.createElement("div"),b.innerHTML='<v:shape adj="1" />',c=b.firstChild,c.style.behavior="url(#default#VML)";return c?typeof c.adj==="object":!0}(document),!pv.have_SVG&&pv.have_VML&&function(){function B(a){var b=a;a=new A(b);for(var c=0,d=z.length;c<d;c++){var e=z[c];a[e]=b[e]}a.target||(a.target=a.srcElement||document),a.target.nodeType===3&&(a.target=a.target.parentNode),!a.relatedTarget&&a.fromElement&&(a.relatedTarget=a.fromElement===a.target?a.toElement:a.fromElement);if(a.pageX==null&&a.clientX!=null){var f=document.documentElement,g=document.body;a.pageX=a.clientX+(f&&f.scrollLeft||g&&g.scrollLeft||0)-(f&&f.clientLeft||g&&g.clientLeft||0),a.pageY=a.clientY+(f&&f.scrollTop||g&&g.scrollTop||0)-(f&&f.clientTop||g&&g.clientTop||0)}a.which==null&&(a.charCode!=null||a.keyCode!=null)&&(a.which=a.charCode!=null?a.charCode:a.keyCode),!a.metaKey&&a.ctrlKey&&(a.metaKey=a.ctrlKey),!a.which&&a.button!==undefined&&(a.which=a.button&1?1:a.button&2?3:a.button&4?2:0);return a}function A(a){if(a&&a.type){this.originalEvent=a,this.type=a.type,this.isDefaultPrevented=y;if(a.defaultPrevented||a.returnValue===!1||a.getPreventDefault&&a.getPreventDefault())this.isDefaultPrevented=x}else this.type=a;this.timeStamp=new Date*1}function m(a,b){var c=a.getElementsByTagName("stroke")[0];c||(c=j("stroke"),a.appendChild(c)),!b.stroke||b.stroke==="none"?(c.on="false",c.weight="0"):(c.on="true",c.weight=parseFloat(b["stroke-width"]||"1")/1.25,c.color=f(b.stroke)||"black",c.opacity=parseFloat(b["stroke-opacity"]||"1")||"1",c.joinstyle=v[b["stroke-linejoin"]]||"miter")}function l(a,b){var c=a.getElementsByTagName("fill")[0];c||(c=j("fill"),a.appendChild(c)),!b.fill||b.fill==="none"?c.on=!1:(c.on="true",c.color=f(b.fill),c.opacity=parseFloat(b["fill-opacity"]||"1")||"1")}function k(a,b){var c=b||{};c.translate_x=0,c.translate_y=0;if(a.transform){var d=/translate\((\d+(?:\.\d+)?)(?:,(\d+(?:\.\d+)?))?\)/.exec(a.transform);d&&d[1]&&(c.translate_x=parseFloat(d[1])),d&&d[2]&&(c.translate_y=parseFloat(d[2]));var e=/rotate\((\d+\.\d+|\d+)\)/.exec(a.transform);e&&(c.rotation=parseFloat(e[1])%360)}c.x=parseFloat(a.x||0),c.y=parseFloat(a.y||0),"width"in a&&(c.width=parseInt(a.width,10)),"height"in a&&(c.height=parseInt(a.height,10));return c}function j(a){a in i||(i[a]=document.createElement(q+a+r));return i[a].cloneNode(!1)}function h(a,b){var c=0,d=0,e=Math.round;a=a.replace(/(\d*)((\.*\d*)(e ?-?\d*))/g,"$1");if(a in g)return g[a];var f=a.match(/([MLHVCSQTAZ].*?)(?=[MLHVCSQTAZ]|$)/gi),h=[];for(var i=0,j=f.length;i<j;i++){var k=f[i],l=k.charAt(0),m=k.substring(1).split(/[, ]/);switch(l){case"M":l="m",c=e(m[0]),d=e(m[1]),m=[c,d];break;case"m":l="m",c+=e(m[0]),d+=e(m[1]),m=[c,d];break;case"A":c=e(m[5]),d=e(m[6]),l="l",m=[c,d];break;case"L":l="l",c=e(m[0]),d=e(m[1]),m=[c,d];break;case"l":l="l",c+=e(m[0]),d+=e(m[1]),m=[c,d];break;case"H":l="l",c=e(m[0]),m=[c,d];break;case"h":l="l",c+=e(m[0]),m=[c,d];break;case"V":l="l",d=e(m[0]),m=[c,d];break;case"v":l="l",d+=e(m[0]),m=[c,d];break;case"C":l="l",c=e(m[4]),d=e(m[5]),m=[c,d];break;case"c":l="l",c+=e(m[4]),d+=e(m[5]),m=[c,d];break;case"Z":case"z":l="xe",m=[];default:}h.push(l+m.join(","))}return g[a]=h.join("")+"e"}function f(a,b){!(a in c)&&(b=/^rgb\((\d+),(\d+),(\d+)\)$/i.exec(a))&&(c[a]="#"+d[b[1]]+d[b[2]]+d[b[3]]);return c[a]||a}pv.VmlScene={scale:1,events:["DOMMouseScroll","mousewheel","mousedown","mouseup","mouseover","mouseout","mousemove","click","dblclick"],implicit:{css:{font:"10px sans-serif"}}};for(var a in pv.SvgScene)a in pv.SvgScene&&typeof pv.SvgScene[a]==="function"&&!(a in pv.VmlScene)&&(pv.VmlScene[a]=pv.SvgScene[a]);pv.Scene=pv.VmlScene;var b={crosshair:1,pointer:1,move:1,hand:"pointer",text:1,wait:1,help:1,progress:1,"n-resize":1,"ne-resize":1,"nw-resize":1,"s-resize":1,"se-resize":1,"sw-resize":1,"e-resize":1,"w-resize":1},c={},d=[];for(var e=0;e<256;e++)d[e]=e===0?"00":e<16?"0"+e.toString(16):e.toString(16);var g={},i={span:document.createElement("span"),div:document.createElement("div")},n=Math.PI*2/360,o=null,p=null,q="<v:",r=' class="msvml">',s="px",t={group:1,shape:1,shapetype:1,line:1,polyline:1,curve:1,rect:1,roundrect:1,oval:1,arc:1,image:1},u={butt:"flat",round:"round",square:"square",flat:"flat"},v={bevel:"bevel",round:"round",miter:"miter"},w={g:{rewrite:"div",attr:function(a,b,c){var d=k(a);c.style.position="absolute",c.style.zoom=1,c.style.left=d.translate_x+d.x+s,c.style.top=d.translate_y+d.y+s}},line:{rewrite:"shape",attr:function(a,b,c){var d=parseFloat(a.x1||0),e=parseFloat(a.y1||0),f=parseFloat(a.x2||0),g=parseFloat(a.y2||0);c.style.top=0+s,c.style.left=0+s,c.style.width=1e3+s,c.style.height=1e3+s;var h=c.getElementsByTagName("path")[0];h||(h=j("path"),c.appendChild(h));var i=Math.round;h.v="M "+i(d)+" "+i(e)+" L "+i(f)+" "+i(g)+" E",m(c,a)}},rect:{rewrite:"rect",attr:function(a,b,c){var d=k(a),e=c.style;e.position="absolute",e.left=d.translate_x+d.x+s,e.top=d.translate_y+d.y+s,d.width&&(e.width=d.width+s),d.height&&(e.height=d.height+s),l(c,a),m(c,a)}},path:{rewrite:"shape",attr:function(a,b,c){var d=k(a);c.style.left=d.translate_x+d.x+s,c.style.top=d.translate_y+d.y+s,c.style.width=1e3+s,c.style.height=1e3+s;var e=c.getElementsByTagName("path")[0];e||(e=j("path"),c.appendChild(e)),e.v=h(a.d),l(c,a),m(c,a)}},circle:{rewrite:"oval",attr:function(a,b,c){var d=k(a),e=parseFloat(a.cx||0),f=parseFloat(a.cy||0),g=parseFloat(a.r||0);c.style.top=d.translate_y+f-g+s,c.style.left=d.translate_x+e-g+s,c.style.width=g*2+s,c.style.height=g*2+s,l(c,a),m(c,a)}},text:{rewrite:"span"},svg:{rewrite:"span",oncreate:function(a){p||(p=document.createElement("span"),p.style.cssText="position:absolute;left:-9999em;top:-9999em;padding:0;margin:0;line-height:1;display:inline-block;white-space:nowrap;",document.body.appendChild(p));if(!b){var b=document.createElement("style");b.id="protovis_vml_styles",document.documentElement.firstChild.appendChild(b),b.styleSheet.addRule(".msvml","behavior:url(#default#VML);"),b.styleSheet.addRule(".msvml_block","position:absolute;top:0;left:0;");try{document.namespaces.v||document.namespaces.add("v","urn:schemas-microsoft-com:vml")}catch(c){q="<",r=' class="msvml" xmlns="urn:schemas-microsoft.com:vml">'}}},css:"position:relative;overflow:hidden;display:inline-block;~display:block;"}},x=function(){return!0},y=function(){return!1},z=["altKey","attrChange","attrName","bubbles","button","cancelable","charCode","clientX","clientY","ctrlKey","currentTarget","data","detail","eventPhase","fromElement","handler","keyCode","layerX","layerY","metaKey","newValue","offsetX","offsetY","pageX","pageY","prevValue","relatedNode","relatedTarget","screenX","screenY","shiftKey","srcElement","target","toElement","view","wheelDelta","which"];A.prototype={preventDefault:function(){this.isDefaultPrevented=x;var a=this.originalEvent;!a||(a.preventDefault?a.preventDefault():a.returnValue=!1)},stopPropagation:function(){this.isPropagationStopped=x;var a=this.originalEvent;!a||(a.stopPropagation&&a.stopPropagation(),a.cancelBubble=!0)},stopImmediatePropagation:function(){this.isImmediatePropagationStopped=x,this.stopPropagation()},isDefaultPrevented:y,isPropagationStopped:y,isImmediatePropagationStopped:y},pv.listener=function(a,b){return a.$listener||(a.$listener=function(b){try{pv.event=B(b||window.event);if(1)return a.call(this,pv.event)}finally{delete pv.event}})},pv.listen=function(a,b,c){c=pv.listener(c,a);return a.addEventListener?a.addEventListener(b,c,!1):a.attachEvent("on"+b,c)},pv.VmlScene.updateAll=function(a){if(a.length&&a[0].reverse&&a.type!="line"&&a.type!="area"){var b=pv.extend(a);for(var c=0,d=a.length-1;d>=0;c++,d--)b[c]=a[d];a=b}this.removeSiblings(this[a.type](a))},pv.VmlScene.create=function(a){var b;if(a in w){var c=w[a]||{},d=c.rewrite||a;b=j(d),b.style.cssText=c.css||"";if(d in t||a==="span"||a==="div")b.className+=" msvml_block";"oncreate"in c&&c.oncreate(b)}else b=j(d),a in t&&(b.className+=" msvml_block");a!==d&&(b.svgtype=a);return b},pv.VmlScene.expect=function(a,c,d,e){e=e||{};var f=w[c]||{},g=f.rewrite||c;if(a){if(a.tagName.toUpperCase()!==g.toUpperCase()){var h=this.create(c);a.parentNode.replaceChild(h,a),a=h}}else a=this.create(c);f.attr&&f.attr(d,e,a),d.cursor in b&&(e.cursor=b[d.cursor]===1?d.cursor:b[d.cursor]);for(var i in e){var j=e[i];j==null?a.style.removeAttribute(i):a.style[i]=j}return a},pv.VmlScene.append=function(a,b,c){a.$scene={scenes:b,index:c},a=this.title(a,b[c]);if(!a.parentNode||a.parentNode.nodeType===11){b.$g.appendChild(a);var d=w[a.svgtype];d&&typeof d.onappend==="function"&&d.onappend(a,b[c])}return a.nextSibling},pv.VmlScene.title=function(a,b){b.title&&(a.title=b.title);return a},pv.VmlScene.dispatch=pv.listener(function(a){var b=a.target.$scene;if(b){var c=a.type;switch(c){case"DOMMouseScroll":c="mousewheel",a.wheel=-480*a.detail;break;case"mousewheel":a.wheel=(window.opera?12:1)*a.wheelDelta}pv.Mark.dispatch(c,b.scenes,b.index)&&a.preventDefault()}}),pv.VmlScene.panel=function(a){var b=a.$g,c=b&&b.firstChild;for(var d=0;d<a.length;d++){var e=a[d];if(!e.visible)continue;if(!a.parent){e.canvas.style.display="inline-block",e.canvas.style.zoom=1,b&&b.parentNode!=e.canvas&&(b=e.canvas.firstChild,c=b&&b.firstChild);if(!b){b=e.canvas.appendChild(this.create("svg"));for(var f=0;f<this.events.length;f++)b.addEventListener?b.addEventListener(this.events[f],this.dispatch,!1):b.attachEvent("on"+this.events[f],this.dispatch);c=b.firstChild}a.$g=b;var g=e.width+e.left+e.right,h=e.height+e.top+e.bottom;b.style.width=g+s,b.style.height=h+s,b.style.clip="rect(0 "+g+s+" "+h+s+" 0)"}c=this.fill(c,a,d);var i=this.scale,j=e.transform,k=e.left+j.x,l=e.top+j.y;this.scale*=j.k;for(var f=0;f<e.children.length;f++){e.children[f].$g=c=this.expect(c,"g",{transform:"translate("+k+","+l+")"+(j.k!=1?" scale("+j.k+")":"")}),this.updateAll(e.children[f]);if(!c.parentNode||c.parentNode.nodeType===11){b.appendChild(c);var m=w[c.svgtype];m&&typeof m.onappend==="function"&&m.onappend(c,a[d])}c=c.nextSibling}this.scale=i,c=this.stroke(c,a,d)}return c},pv.VmlScene.image=function(a){var b=a.$g.firstChild;for(var c=0;c<a.length;c++){var d=a[c];if(!d.visible)continue;b=this.fill(b,a,c);if(!d.image){b=new Image,b.src=d.url;var e=b.style;e.position="absolute",e.top=d.top,e.left=d.left,e.width=d.width,e.height=d.height,e.cursor=d.cursor,e.msInterpolationMode="bicubic"}b=this.append(b,a,c),b=this.stroke(b,a,c)}return b},pv.VmlScene.label=function(a){var b=a.$g.firstChild;for(var c=0;c<a.length;c++){var d=a[c];if(!d.visible)continue;var e=d.textStyle;if(!e.opacity||!d.text)continue;var g={};d.cursor&&(g.cursor=d.cursor),b=this.expect(b,"text",g,{font:d.font,textDecoration:d.textDecoration,top:d.top+s,left:d.left+s,position:"absolute",display:"block",lineHeight:1,whiteSpace:"nowrap",zoom:1,cursor:"default",color:f(e.color)||"black"});var h=180*d.textAngle/Math.PI;if(h){var i=~~h%360*n,j=Math.cos(i),k=Math.sin(i);b.style.filter="progid:DXImageTransform.Microsoft.Matrix(M11="+j.toFixed(8)+","+"M12="+ -k.toFixed(8)+","+"M21="+k.toFixed(8)+","+"M22="+j.toFixed(8)+",sizingMethod='auto expand')\";"}var l=d.text.replace(/\s+/g,"Â ");p.style.font=d.font,p.innerText=l;var m=b.style;d.textAlign==="center"?m.marginLeft=-Math.ceil(p.offsetWidth/2)+s:d.textAlign==="right"?m.marginLeft=-(p.offsetWidth+d.textMargin)+s:d.textAlign==="left"&&(m.marginLeft=d.textMargin+s),d.textBaseline==="middle"?m.marginTop=-Math.floor(p.offsetHeight*.45)+s:d.textBaseline==="top"?m.marginTop=1+s:d.textBaseline==="bottom"&&(m.marginTop=-p.offsetHeight+s),b.innerText=l,b=this.append(b,a,c)}return b},"now"in Date||(Date.now=function(){return(new Date).valueOf()})}()
\ No newline at end of file diff --git a/sonar-server/src/main/webapp/javascripts/protovis-sonar.js b/sonar-server/src/main/webapp/javascripts/protovis-sonar.js new file mode 100755 index 00000000000..95547a43268 --- /dev/null +++ b/sonar-server/src/main/webapp/javascripts/protovis-sonar.js @@ -0,0 +1,410 @@ +window.SonarWidgets = {} + + +//******************* STACK AREA CHART ******************* // + +SonarWidgets.StackArea = function (divId) { + this.wDivId = divId; + this.wHeight; + this.wData; + this.wSnapshots; + this.wMetrics; + this.wColors; + this.height = function(height) { + this.wHeight = height; + return this; + } + this.data = function(data) { + this.wData = data; + return this; + } + this.snapshots = function(snapshots) { + this.wSnapshots = snapshots; + return this; + } + this.metrics = function(metrics) { + this.wMetrics = metrics; + return this; + } + this.colors = function(colors) { + this.wColors = colors; + return this; + } +} + +SonarWidgets.StackArea.prototype.render = function() { + + var trendData = this.wData; + var metrics = this.wMetrics; + var snapshots = this.wSnapshots; + var colors = this.wColors; + + var widgetDiv = $(this.wDivId); + var headerFont = "10.5px Arial,Helvetica,sans-serif"; + + /* Computes the total of the trendData of each date */ + var total = []; + for (i=0; i<trendData[0].size(); i++) { + total[i] = 0; + for (j=0; j<metrics.size();j++) { + total[i] += trendData[j][i].y; + } + total[i] = "" + Math.round(total[i]*10)/10 + } + + /* Computes the highest Y value */ + var maxY = 0; + for (i=0; i<trendData[0].size(); i++) { + var currentYSum = 0; + for (j=0; j<trendData.size(); j++) { + currentYSum += trendData[j][i].y; + } + if (currentYSum > maxY) { maxY = currentYSum;} + } + + /* Computes minimum width of left margin according to the max Y value so that the Y-axis is correctly displayed */ + var leftMargin = 25; + var maxYLength = (Math.round(maxY) + "").length; + minMargin = maxYLength * 7 + Math.floor(maxYLength /3) * 2; // first part is for numbers and second for commas (1000-separator) + if (minMargin > leftMargin) { leftMargin = minMargin; } + + /* Sizing and scales. */ + var headerHeight = 40; + var w = widgetDiv.getOffsetParent().getWidth() - leftMargin - 40; + var h = (this.wHeight == null ? 200 : this.wHeight) + headerHeight; + + var x = pv.Scale.linear(pv.blend(pv.map(trendData, function(d) {return d;})), function(d) {return d.x}).range(0, w); + var y = pv.Scale.linear(0, maxY).range(0, h-headerHeight); + var idx_numbers = trendData[0].size(); + var idx = idx_numbers - 1; + + function computeIdx(xPixels) { + var mx = x.invert(xPixels); + var i = pv.search(trendData[0].map(function(d) {return d.x;}), mx); + i = i < 0 ? (-i - 2) : i; + i = i < 0 ? 0 : i; + return i; + } + + /* The root panel. */ + var vis = new pv.Panel() + .canvas(widgetDiv) + .width(w) + .height(h) + .left(leftMargin) + .right(20) + .bottom(30) + .top(20) + .strokeStyle("#CCC"); + + /* X-axis */ + vis.add(pv.Rule) + .data(x.ticks()) + .left(x) + .bottom(-10) + .height(10) + .anchor("bottom") + .add(pv.Label) + .text(x.tickFormat); + + /* Y-axis and ticks. */ + vis.add(pv.Rule) + .data(y.ticks(6)) + .bottom(y) + .strokeStyle("rgba(128,128,128,.2)") + .anchor("left") + .add(pv.Label) + .text(y.tickFormat); + + /* The stack layout */ + var area = vis.add(pv.Layout.Stack) + .layers(trendData) + .x(function(d) {return x(d.x);}) + .y(function(d) {return y(d.y);}) + .layer + .add(pv.Area) + .fillStyle(function() {return colors[this.parent.index % colors.size()][0];}) + .strokeStyle("rgba(128,128,128,.8)"); + + /* Stack labels. */ + var firstIdx = computeIdx(w/5); + var lastIdx = computeIdx(w*4/5); + vis.add(pv.Panel) + .extend(area.parent) + .add(pv.Area) + .extend(area) + .fillStyle(null) + .strokeStyle(null) + .anchor(function() {return (idx==idx_numbers-1 || idx > lastIdx) ? "right" : ((idx==0 || idx < firstIdx) ? "left" : "center");}) + .add(pv.Label) + .visible(function(d) {return this.index == idx && d.y != 0;}) + .font(function(d) { return Math.round(5 + Math.sqrt(y(d.y))) + "px sans-serif";}) + .textStyle("#DDD") + .text(function(d) {return metrics[this.parent.index] + ": " + d.y;}); + + /* The total cost of the selected dot in the header. */ + vis.add(pv.Label) + .left(8) + .top(16) + .font(headerFont) + .text(function() {return "Total: " + total[idx];}); + + /* The date of the selected dot in the header. */ + vis.add(pv.Label) + .left(w/2) + .top(16) + .font(headerFont) + .text(function() {return snapshots[idx].ld;}); + + + /* The event labels */ + if (true) { + eventColor = "rgba(75,159,213,1)"; + eventHoverColor = "rgba(202,227,242,1)"; + vis.add(pv.Line) + .strokeStyle("rgba(0,0,0,.001)") + .data(snapshots) + .left(function(s) {return x(s.d);}) + .bottom(0) + .anchor("top") + .add(pv.Dot) + .bottom(-6) + .shape("triangle") + .angle(pv.radians(180)) + .strokeStyle("grey") + .visible(function(s) {return s.e.size() > 0;}) + .fillStyle(function() {return this.index == idx ? eventHoverColor : eventColor;}) + .add(pv.Dot) + .radius(3) + .visible(function(s) {return s.e.size() > 0 && this.index == idx;}) + .left(w/2+8) + .top(24) + .shape("triangle") + .fillStyle(function() {return this.index == idx ? eventHoverColor : eventColor;}) + .strokeStyle("grey") + .anchor("right") + .add(pv.Label) + .font(headerFont) + .text(function(s) {return s.e.size() == 0 ? "" : s.e[0] + ( s.e[1] ? " (... +" + (s.e.size()-1) + ")" : "");}); + } + + /* An invisible bar to capture events (without flickering). */ + vis.add(pv.Bar) + .fillStyle("rgba(0,0,0,.001)") + .width(w+30) + .height(h+30) + .event("mouseout", function() { + i = -1; + return vis; + }) + .event("mousemove", function() { + idx = computeIdx(vis.mouse().x); + return vis; + }); + + vis.render(); + +} + + + +//******************* TIMELINE CHART ******************* // +/* + * Displays the evolution of metrics on a line chart, displaying related events. + * + * Parameters of the Timeline class: + * - data: array of arrays, each containing maps {x,y} where x is a (JS) date and y is a number value (representing a metric value at + * a given time). The {x,y} maps must be sorted by ascending date. + * - metrics: array of metric names. The order is important as it defines which array of the "data" parameter represents which metric. + * - snapshots: array of maps {sid,d} where sid is the snapshot id and d is the locale-formatted date of the snapshot. The {sid,d} + * maps must be sorted by ascending date. + * - events: array of maps {sid,d,l[{n}]} where sid is the snapshot id corresponding to an event, d is the (JS) date of the event, and l + * is an array containing the different event names for this date. + * - height: height of the chart area (notice header excluded). Defaults to 80. + * + * Example: displays 2 metrics: + * +<code> +function d(y,m,d,h,min,s) { + return new Date(y,m,d,h,min,s); +} +var data = [ + [{x:d(2011,5,15,0,1,0),y:912.00},{x:d(2011,6,21,0,1,0)], + [{x:d(2011,5,15,0,1,0),y:52.20},{x:d(2011,6,21,0,1,0),y:52.10}] + ]; +var metrics = ["Lines of code","Rules compliance"]; +var snapshots = [{sid:1,d:"June 15, 2011 00:01"},{sid:30,d:"July 21, 2011 00:01"}]; +var events = [ + {sid:1,d:d(2011,5,15,0,1,0),l:[{n:"0.6-SNAPSHOT"},{n:"Sun checks"}]}, + {sid:30,d:d(2011,6,21,0,1,0),l:[{n:"0.7-SNAPSHOT"}]} + ]; + +var timeline = new SonarWidgets.Timeline('timeline-chart-20') + .height(160) + .data(data) + .snapshots(snapshots) + .metrics(metrics) + .events(events); +timeline.render(); +</code> + * + */ + +SonarWidgets.Timeline = function (divId) { + this.wDivId = divId; + this.wHeight; + this.wData; + this.wSnapshots; + this.wMetrics; + this.wEvents; + this.height = function(height) { + this.wHeight = height; + return this; + } + this.data = function(data) { + this.wData = data; + return this; + } + this.snapshots = function(snapshots) { + this.wSnapshots = snapshots; + return this; + } + this.metrics = function(metrics) { + this.wMetrics = metrics; + return this; + } + this.events = function(events) { + this.wEvents = events; + return this; + } +} + +SonarWidgets.Timeline.prototype.render = function() { + + var trendData = this.wData; + var metrics = this.wMetrics; + var snapshots = this.wSnapshots; + var events = this.wEvents; + + var widgetDiv = $(this.wDivId); + var headerFont = "10.5px Arial,Helvetica,sans-serif"; + + /* Sizing and scales. */ + var headerHeight = 4 + Math.max(this.wMetrics.size(), events ? 2 : 1) * 18; + var w = widgetDiv.getOffsetParent().getWidth() - 60; + var h = (this.wHeight == null ? 80 : this.wHeight) + headerHeight; + var yMaxHeight = h-headerHeight; + + var x = pv.Scale.linear(pv.blend(pv.map(trendData, function(d) {return d;})), function(d) {return d.x}).range(0, w); + var y = new Array(trendData.size()); + for(var i = 0; i < trendData.size(); i++){ + y[i]=pv.Scale.linear(trendData[i], function(d) {return d.y;}).range(20, yMaxHeight); + } + var interpolate = "linear"; /* cardinal or linear */ + var idx = trendData[0].size() - 1; + + /* The root panel. */ + var vis = new pv.Panel() + .canvas(widgetDiv) + .width(w) + .height(h) + .left(20) + .right(20) + .bottom(30) + .top(5) + .strokeStyle("#CCC"); + + /* X-axis */ + vis.add(pv.Rule) + .data(x.ticks()) + .left(x) + .bottom(-10) + .height(10) + .anchor("bottom") + .add(pv.Label) + .text(x.tickFormat); + + /* A panel for each data series. */ + var panel = vis.add(pv.Panel) + .data(trendData); + + /* The line. */ + var line = panel.add(pv.Line) + .data(function(array) {return array;}) + .left(function(d) {return x(d.x);}) + .bottom(function(d) {var yAxis = y[this.parent.index](d.y); return isNaN(yAxis) ? yMaxHeight : yAxis;}) + .interpolate(function() {return interpolate;}) + .lineWidth(2); + + /* The mouseover dots and label in footer. */ + line.add(pv.Dot) + .data(function(d) {return [d[idx]];}) + .fillStyle(function() {return line.strokeStyle();}) + .strokeStyle("#000") + .size(20) + .lineWidth(1) + .add(pv.Dot) + .radius(3) + .left(10) + .top(function() {return 10 + this.parent.index * 14;}) + .anchor("right").add(pv.Label) + .font(headerFont) + .text(function(d) {return metrics[this.parent.index] + ": " + d.y.toFixed(2);}); + + /* The date of the selected dot in the header. */ + vis.add(pv.Label) + .left(w/2) + .top(16) + .font(headerFont) + .text(function() {return snapshots[idx].d;}); + + /* The event labels */ + if (events) { + eventColor = "rgba(75,159,213,1)"; + eventHoverColor = "rgba(202,227,242,1)"; + vis.add(pv.Line) + .strokeStyle("rgba(0,0,0,.001)") + .data(events) + .left(function(e) {return x(e.d);}) + .bottom(0) + .anchor("top") + .add(pv.Dot) + .bottom(-6) + .shape("triangle") + .angle(pv.radians(180)) + .strokeStyle("grey") + .fillStyle(function(e) {return e.sid == snapshots[idx].sid ? eventHoverColor : eventColor;}) + .add(pv.Dot) + .radius(3) + .visible(function(e) { return e.sid == snapshots[idx].sid;}) + .left(w/2+8) + .top(24) + .shape("triangle") + .fillStyle(function(e) {return e.sid == snapshots[idx].sid ? eventHoverColor : eventColor;}) + .strokeStyle("grey") + .anchor("right") + .add(pv.Label) + .font(headerFont) + .text(function(e) {return e.l[0].n + ( e.l[1] ? " (... +" + (e.l.size()-1) + ")" : "");}); + } + + /* An invisible bar to capture events (without flickering). */ + vis.add(pv.Bar) + .fillStyle("rgba(0,0,0,.001)") + .width(w+30) + .height(h+30) + .event("mouseout", function() { + i = -1; + return vis; + }) + .event("mousemove", function() { + var mx = x.invert(vis.mouse().x); + idx = pv.search(trendData[0].map(function(d) {return d.x;}), mx); + idx = idx < 0 ? (-idx - 2) : idx; + idx = idx < 0 ? 0 : idx; + return vis; + }); + + vis.render(); + +}
\ No newline at end of file diff --git a/sonar-server/src/main/webapp/javascripts/protovis.js b/sonar-server/src/main/webapp/javascripts/protovis.js new file mode 100755 index 00000000000..c3eff747910 --- /dev/null +++ b/sonar-server/src/main/webapp/javascripts/protovis.js @@ -0,0 +1,15326 @@ +/** + * @class The built-in Array class. + * @name Array + */ + +/** + * Creates a new array with the results of calling a provided function on every + * element in this array. Implemented in Javascript 1.6. + * + * @function + * @name Array.prototype.map + * @see <a + * href="https://developer.mozilla.org/En/Core_JavaScript_1.5_Reference/Objects/Array/Map">map</a> + * documentation. + * @param {function} f function that produces an element of the new Array from + * an element of the current one. + * @param [o] object to use as <tt>this</tt> when executing <tt>f</tt>. + */ +if (!Array.prototype.map) Array.prototype.map = function(f, o) { + var n = this.length; + var result = new Array(n); + for (var i = 0; i < n; i++) { + if (i in this) { + result[i] = f.call(o, this[i], i, this); + } + } + return result; +}; + +/** + * Creates a new array with all elements that pass the test implemented by the + * provided function. Implemented in Javascript 1.6. + * + * @function + * @name Array.prototype.filter + * @see <a + * href="https://developer.mozilla.org/En/Core_JavaScript_1.5_Reference/Objects/Array/filter">filter</a> + * documentation. + * @param {function} f function to test each element of the array. + * @param [o] object to use as <tt>this</tt> when executing <tt>f</tt>. + */ +if (!Array.prototype.filter) Array.prototype.filter = function(f, o) { + var n = this.length; + var result = new Array(); + for (var i = 0; i < n; i++) { + if (i in this) { + var v = this[i]; + if (f.call(o, v, i, this)) result.push(v); + } + } + return result; +}; + +/** + * Executes a provided function once per array element. Implemented in + * Javascript 1.6. + * + * @function + * @name Array.prototype.forEach + * @see <a + * href="https://developer.mozilla.org/En/Core_JavaScript_1.5_Reference/Objects/Array/ForEach">forEach</a> + * documentation. + * @param {function} f function to execute for each element. + * @param [o] object to use as <tt>this</tt> when executing <tt>f</tt>. + */ +if (!Array.prototype.forEach) Array.prototype.forEach = function(f, o) { + var n = this.length >>> 0; + for (var i = 0; i < n; i++) { + if (i in this) f.call(o, this[i], i, this); + } +}; + +/** + * Apply a function against an accumulator and each value of the array (from + * left-to-right) as to reduce it to a single value. Implemented in Javascript + * 1.8. + * + * @function + * @name Array.prototype.reduce + * @see <a + * href="https://developer.mozilla.org/En/Core_JavaScript_1.5_Reference/Objects/Array/Reduce">reduce</a> + * documentation. + * @param {function} f function to execute on each value in the array. + * @param [v] object to use as the first argument to the first call of + * <tt>t</tt>. + */ +if (!Array.prototype.reduce) Array.prototype.reduce = function(f, v) { + var len = this.length; + if (!len && (arguments.length == 1)) { + throw new Error("reduce: empty array, no initial value"); + } + + var i = 0; + if (arguments.length < 2) { + while (true) { + if (i in this) { + v = this[i++]; + break; + } + if (++i >= len) { + throw new Error("reduce: no values, no initial value"); + } + } + } + + for (; i < len; i++) { + if (i in this) { + v = f(v, this[i], i, this); + } + } + return v; +}; +/** + * The top-level Protovis namespace. All public methods and fields should be + * registered on this object. Note that core Protovis source is surrounded by an + * anonymous function, so any other declared globals will not be visible outside + * of core methods. This also allows multiple versions of Protovis to coexist, + * since each version will see their own <tt>pv</tt> namespace. + * + * @namespace The top-level Protovis namespace, <tt>pv</tt>. + */ +var pv = {}; + +/** + * Protovis version number. See <a href="http://semver.org">semver.org</a>. + * + * @type string + * @constant + */ +pv.version = "3.3.1"; + +/** + * Returns the passed-in argument, <tt>x</tt>; the identity function. This method + * is provided for convenience since it is used as the default behavior for a + * number of property functions. + * + * @param x a value. + * @returns the value <tt>x</tt>. + */ +pv.identity = function(x) { return x; }; + +/** + * Returns <tt>this.index</tt>. This method is provided for convenience for use + * with scales. For example, to color bars by their index, say: + * + * <pre>.fillStyle(pv.Colors.category10().by(pv.index))</pre> + * + * This method is equivalent to <tt>function() this.index</tt>, but more + * succinct. Note that the <tt>index</tt> property is also supported for + * accessor functions with {@link pv.max}, {@link pv.min} and other array + * utility methods. + * + * @see pv.Scale + * @see pv.Mark#index + */ +pv.index = function() { return this.index; }; + +/** + * Returns <tt>this.childIndex</tt>. This method is provided for convenience for + * use with scales. For example, to color bars by their child index, say: + * + * <pre>.fillStyle(pv.Colors.category10().by(pv.child))</pre> + * + * This method is equivalent to <tt>function() this.childIndex</tt>, but more + * succinct. + * + * @see pv.Scale + * @see pv.Mark#childIndex + */ +pv.child = function() { return this.childIndex; }; + +/** + * Returns <tt>this.parent.index</tt>. This method is provided for convenience + * for use with scales. This method is provided for convenience for use with + * scales. For example, to color bars by their parent index, say: + * + * <pre>.fillStyle(pv.Colors.category10().by(pv.parent))</pre> + * + * Tthis method is equivalent to <tt>function() this.parent.index</tt>, but more + * succinct. + * + * @see pv.Scale + * @see pv.Mark#index + */ +pv.parent = function() { return this.parent.index; }; + +/** + * Stores the current event. This field is only set within event handlers. + * + * @type Event + * @name pv.event + */ +/** + * @private Returns a prototype object suitable for extending the given class + * <tt>f</tt>. Rather than constructing a new instance of <tt>f</tt> to serve as + * the prototype (which unnecessarily runs the constructor on the created + * prototype object, potentially polluting it), an anonymous function is + * generated internally that shares the same prototype: + * + * <pre>function g() {} + * g.prototype = f.prototype; + * return new g();</pre> + * + * For more details, see Douglas Crockford's essay on prototypal inheritance. + * + * @param {function} f a constructor. + * @returns a suitable prototype object. + * @see Douglas Crockford's essay on <a + * href="http://javascript.crockford.com/prototypal.html">prototypal + * inheritance</a>. + */ +pv.extend = function(f) { + function g() {} + g.prototype = f.prototype || f; + return new g(); +}; + +try { + eval("pv.parse = function(x) x;"); // native support +} catch (e) { + +/** + * @private Parses a Protovis specification, which may use JavaScript 1.8 + * function expresses, replacing those function expressions with proper + * functions such that the code can be run by a JavaScript 1.6 interpreter. This + * hack only supports function expressions (using clumsy regular expressions, no + * less), and not other JavaScript 1.8 features such as let expressions. + * + * @param {string} s a Protovis specification (i.e., a string of JavaScript 1.8 + * source code). + * @returns {string} a conformant JavaScript 1.6 source code. + */ + pv.parse = function(js) { // hacky regex support + var re = new RegExp("function\\s*(\\b\\w+)?\\s*\\([^)]*\\)\\s*", "mg"), m, d, i = 0, s = ""; + while (m = re.exec(js)) { + var j = m.index + m[0].length; + if (js.charAt(j) != '{') { + s += js.substring(i, j) + "{return "; + i = j; + for (var p = 0; p >= 0 && j < js.length; j++) { + var c = js.charAt(j); + switch (c) { + case '"': case '\'': { + while (++j < js.length && (d = js.charAt(j)) != c) { + if (d == '\\') j++; + } + break; + } + case '[': case '(': p++; break; + case ']': case ')': p--; break; + case ';': + case ',': if (p == 0) p--; break; + } + } + s += pv.parse(js.substring(i, --j)) + ";}"; + i = j; + } + re.lastIndex = j; + } + s += js.substring(i); + return s; + }; +} + +/** + * @private Computes the value of the specified CSS property <tt>p</tt> on the + * specified element <tt>e</tt>. + * + * @param {string} p the name of the CSS property. + * @param e the element on which to compute the CSS property. + */ +pv.css = function(e, p) { + return window.getComputedStyle + ? window.getComputedStyle(e, null).getPropertyValue(p) + : e.currentStyle[p]; +}; + +/** + * @private Reports the specified error to the JavaScript console. Mozilla only + * allows logging to the console for privileged code; if the console is + * unavailable, the alert dialog box is used instead. + * + * @param e the exception that triggered the error. + */ +pv.error = function(e) { + (typeof console == "undefined") ? alert(e) : console.error(e); +}; + +/** + * @private Registers the specified listener for events of the specified type on + * the specified target. For standards-compliant browsers, this method uses + * <tt>addEventListener</tt>; for Internet Explorer, <tt>attachEvent</tt>. + * + * @param target a DOM element. + * @param {string} type the type of event, such as "click". + * @param {function} the event handler callback. + */ +pv.listen = function(target, type, listener) { + listener = pv.listener(listener); + return target.addEventListener + ? target.addEventListener(type, listener, false) + : target.attachEvent("on" + type, listener); +}; + +/** + * @private Returns a wrapper for the specified listener function such that the + * {@link pv.event} is set for the duration of the listener's invocation. The + * wrapper is cached on the returned function, such that duplicate registrations + * of the wrapped event handler are ignored. + * + * @param {function} f an event handler. + * @returns {function} the wrapped event handler. + */ +pv.listener = function(f) { + return f.$listener || (f.$listener = function(e) { + try { + pv.event = e; + return f.call(this, e); + } finally { + delete pv.event; + } + }); +}; + +/** + * @private Returns true iff <i>a</i> is an ancestor of <i>e</i>. This is useful + * for ignoring mouseout and mouseover events that are contained within the + * target element. + */ +pv.ancestor = function(a, e) { + while (e) { + if (e == a) return true; + e = e.parentNode; + } + return false; +}; + +/** @private Returns a locally-unique positive id. */ +pv.id = function() { + var id = 1; return function() { return id++; }; +}(); + +/** @private Returns a function wrapping the specified constant. */ +pv.functor = function(v) { + return typeof v == "function" ? v : function() { return v; }; +}; +/* + * Parses the Protovis specifications on load, allowing the use of JavaScript + * 1.8 function expressions on browsers that only support JavaScript 1.6. + * + * @see pv.parse + */ +pv.listen(window, "load", function() { + /* + * Note: in Firefox any variables declared here are visible to the eval'd + * script below. Even worse, any global variables declared by the script + * could overwrite local variables here (such as the index, `i`)! To protect + * against this, all variables are explicitly scoped on a pv.$ object. + */ + pv.$ = {i:0, x:document.getElementsByTagName("script")}; + for (; pv.$.i < pv.$.x.length; pv.$.i++) { + pv.$.s = pv.$.x[pv.$.i]; + if (pv.$.s.type == "text/javascript+protovis") { + try { + window.eval(pv.parse(pv.$.s.text)); + } catch (e) { + pv.error(e); + } + } + } + delete pv.$; + }); +/** + * Abstract; see an implementing class. + * + * @class Represents an abstract text formatter and parser. A <i>format</i> is a + * function that converts an object of a given type, such as a <tt>Date</tt>, to + * a human-readable string representation. The format may also have a + * {@link #parse} method for converting a string representation back to the + * given object type. + * + * <p>Because formats are themselves functions, they can be used directly as + * mark properties. For example, if the data associated with a label are dates, + * a date format can be used as label text: + * + * <pre> .text(pv.Format.date("%m/%d/%y"))</pre> + * + * And as with scales, if the format is used in multiple places, it can be + * convenient to declare it as a global variable and then reference it from the + * appropriate property functions. For example, if the data has a <tt>date</tt> + * attribute, and <tt>format</tt> references a given date format: + * + * <pre> .text(function(d) format(d.date))</pre> + * + * Similarly, to parse a string into a date: + * + * <pre>var date = format.parse("4/30/2010");</pre> + * + * Not all format implementations support parsing. See the implementing class + * for details. + * + * @see pv.Format.date + * @see pv.Format.number + * @see pv.Format.time + */ +pv.Format = {}; + +/** + * Formats the specified object, returning the string representation. + * + * @function + * @name pv.Format.prototype.format + * @param {object} x the object to format. + * @returns {string} the formatted string. + */ + +/** + * Parses the specified string, returning the object representation. + * + * @function + * @name pv.Format.prototype.parse + * @param {string} x the string to parse. + * @returns {object} the parsed object. + */ + +/** + * @private Given a string that may be used as part of a regular expression, + * this methods returns an appropriately quoted version of the specified string, + * with any special characters escaped. + * + * @param {string} s a string to quote. + * @returns {string} the quoted string. + */ +pv.Format.re = function(s) { + return s.replace(/[\\\^\$\*\+\?\[\]\(\)\.\{\}]/g, "\\$&"); +}; + +/** + * @private Optionally pads the specified string <i>s</i> so that it is at least + * <i>n</i> characters long, using the padding character <i>c</i>. + * + * @param {string} c the padding character. + * @param {number} n the minimum string length. + * @param {string} s the string to pad. + * @returns {string} the padded string. + */ +pv.Format.pad = function(c, n, s) { + var m = n - String(s).length; + return (m < 1) ? s : new Array(m + 1).join(c) + s; +}; +/** + * Constructs a new date format with the specified string pattern. + * + * @class The format string is in the same format expected by the + * <tt>strftime</tt> function in C. The following conversion specifications are + * supported:<ul> + * + * <li>%a - abbreviated weekday name.</li> + * <li>%A - full weekday name.</li> + * <li>%b - abbreviated month names.</li> + * <li>%B - full month names.</li> + * <li>%c - locale's appropriate date and time.</li> + * <li>%C - century number.</li> + * <li>%d - day of month [01,31] (zero padded).</li> + * <li>%D - same as %m/%d/%y.</li> + * <li>%e - day of month [ 1,31] (space padded).</li> + * <li>%h - same as %b.</li> + * <li>%H - hour (24-hour clock) [00,23] (zero padded).</li> + * <li>%I - hour (12-hour clock) [01,12] (zero padded).</li> + * <li>%m - month number [01,12] (zero padded).</li> + * <li>%M - minute [0,59] (zero padded).</li> + * <li>%n - newline character.</li> + * <li>%p - locale's equivalent of a.m. or p.m.</li> + * <li>%r - same as %I:%M:%S %p.</li> + * <li>%R - same as %H:%M.</li> + * <li>%S - second [00,61] (zero padded).</li> + * <li>%t - tab character.</li> + * <li>%T - same as %H:%M:%S.</li> + * <li>%x - same as %m/%d/%y.</li> + * <li>%X - same as %I:%M:%S %p.</li> + * <li>%y - year with century [00,99] (zero padded).</li> + * <li>%Y - year including century.</li> + * <li>%% - %.</li> + * + * </ul>The following conversion specifications are currently <i>unsupported</i> + * for formatting:<ul> + * + * <li>%j - day number [1,366].</li> + * <li>%u - weekday number [1,7].</li> + * <li>%U - week number [00,53].</li> + * <li>%V - week number [01,53].</li> + * <li>%w - weekday number [0,6].</li> + * <li>%W - week number [00,53].</li> + * <li>%Z - timezone name or abbreviation.</li> + * + * </ul>In addition, the following conversion specifications are currently + * <i>unsupported</i> for parsing:<ul> + * + * <li>%a - day of week, either abbreviated or full name.</li> + * <li>%A - same as %a.</li> + * <li>%c - locale's appropriate date and time.</li> + * <li>%C - century number.</li> + * <li>%D - same as %m/%d/%y.</li> + * <li>%I - hour (12-hour clock) [1,12].</li> + * <li>%n - any white space.</li> + * <li>%p - locale's equivalent of a.m. or p.m.</li> + * <li>%r - same as %I:%M:%S %p.</li> + * <li>%R - same as %H:%M.</li> + * <li>%t - same as %n.</li> + * <li>%T - same as %H:%M:%S.</li> + * <li>%x - locale's equivalent to %m/%d/%y.</li> + * <li>%X - locale's equivalent to %I:%M:%S %p.</li> + * + * </ul> + * + * @see <a + * href="http://www.opengroup.org/onlinepubs/007908799/xsh/strftime.html">strftime</a> + * documentation. + * @see <a + * href="http://www.opengroup.org/onlinepubs/007908799/xsh/strptime.html">strptime</a> + * documentation. + * @extends pv.Format + * @param {string} pattern the format pattern. + */ +pv.Format.date = function(pattern) { + var pad = pv.Format.pad; + + /** @private */ + function format(d) { + return pattern.replace(/%[a-zA-Z0-9]/g, function(s) { + switch (s) { + case '%a': return [ + "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" + ][d.getDay()]; + case '%A': return [ + "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", + "Saturday" + ][d.getDay()]; + case '%h': + case '%b': return [ + "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", + "Oct", "Nov", "Dec" + ][d.getMonth()]; + case '%B': return [ + "January", "February", "March", "April", "May", "June", "July", + "August", "September", "October", "November", "December" + ][d.getMonth()]; + case '%c': return d.toLocaleString(); + case '%C': return pad("0", 2, Math.floor(d.getFullYear() / 100) % 100); + case '%d': return pad("0", 2, d.getDate()); + case '%x': + case '%D': return pad("0", 2, d.getMonth() + 1) + + "/" + pad("0", 2, d.getDate()) + + "/" + pad("0", 2, d.getFullYear() % 100); + case '%e': return pad(" ", 2, d.getDate()); + case '%H': return pad("0", 2, d.getHours()); + case '%I': { + var h = d.getHours() % 12; + return h ? pad("0", 2, h) : 12; + } + // TODO %j: day of year as a decimal number [001,366] + case '%m': return pad("0", 2, d.getMonth() + 1); + case '%M': return pad("0", 2, d.getMinutes()); + case '%n': return "\n"; + case '%p': return d.getHours() < 12 ? "AM" : "PM"; + case '%T': + case '%X': + case '%r': { + var h = d.getHours() % 12; + return (h ? pad("0", 2, h) : 12) + + ":" + pad("0", 2, d.getMinutes()) + + ":" + pad("0", 2, d.getSeconds()) + + " " + (d.getHours() < 12 ? "AM" : "PM"); + } + case '%R': return pad("0", 2, d.getHours()) + ":" + pad("0", 2, d.getMinutes()); + case '%S': return pad("0", 2, d.getSeconds()); + case '%Q': return pad("0", 3, d.getMilliseconds()); + case '%t': return "\t"; + case '%u': { + var w = d.getDay(); + return w ? w : 1; + } + // TODO %U: week number (sunday first day) [00,53] + // TODO %V: week number (monday first day) [01,53] ... with weirdness + case '%w': return d.getDay(); + // TODO %W: week number (monday first day) [00,53] ... with weirdness + case '%y': return pad("0", 2, d.getFullYear() % 100); + case '%Y': return d.getFullYear(); + // TODO %Z: timezone name or abbreviation + case '%%': return "%"; + } + return s; + }); + } + + /** + * Converts a date to a string using the associated formatting pattern. + * + * @function + * @name pv.Format.date.prototype.format + * @param {Date} date a date to format. + * @returns {string} the formatted date as a string. + */ + format.format = format; + + /** + * Parses a date from a string using the associated formatting pattern. + * + * @function + * @name pv.Format.date.prototype.parse + * @param {string} s the string to parse as a date. + * @returns {Date} the parsed date. + */ + format.parse = function(s) { + var year = 1970, month = 0, date = 1, hour = 0, minute = 0, second = 0; + var fields = [function() {}]; + + /* Register callbacks for each field in the format pattern. */ + var re = pv.Format.re(pattern).replace(/%[a-zA-Z0-9]/g, function(s) { + switch (s) { + // TODO %a: day of week, either abbreviated or full name + // TODO %A: same as %a + case '%b': { + fields.push(function(x) { month = { + Jan: 0, Feb: 1, Mar: 2, Apr: 3, May: 4, Jun: 5, Jul: 6, Aug: 7, + Sep: 8, Oct: 9, Nov: 10, Dec: 11 + }[x]; }); + return "([A-Za-z]+)"; + } + case '%h': + case '%B': { + fields.push(function(x) { month = { + January: 0, February: 1, March: 2, April: 3, May: 4, June: 5, + July: 6, August: 7, September: 8, October: 9, November: 10, + December: 11 + }[x]; }); + return "([A-Za-z]+)"; + } + // TODO %c: locale's appropriate date and time + // TODO %C: century number[0,99] + case '%e': + case '%d': { + fields.push(function(x) { date = x; }); + return "([0-9]+)"; + } + // TODO %D: same as %m/%d/%y + case '%I': + case '%H': { + fields.push(function(x) { hour = x; }); + return "([0-9]+)"; + } + // TODO %j: day number [1,366] + case '%m': { + fields.push(function(x) { month = x - 1; }); + return "([0-9]+)"; + } + case '%M': { + fields.push(function(x) { minute = x; }); + return "([0-9]+)"; + } + // TODO %n: any white space + // TODO %p: locale's equivalent of a.m. or p.m. + case '%p': { // TODO this is a hack + fields.push(function(x) { + if (hour == 12) { + if (x == "am") hour = 0; + } else if (x == "pm") { + hour = Number(hour) + 12; + } + }); + return "(am|pm)"; + } + // TODO %r: %I:%M:%S %p + // TODO %R: %H:%M + case '%S': { + fields.push(function(x) { second = x; }); + return "([0-9]+)"; + } + // TODO %t: any white space + // TODO %T: %H:%M:%S + // TODO %U: week number [00,53] + // TODO %w: weekday [0,6] + // TODO %W: week number [00, 53] + // TODO %x: locale date (%m/%d/%y) + // TODO %X: locale time (%I:%M:%S %p) + case '%y': { + fields.push(function(x) { + x = Number(x); + year = x + (((0 <= x) && (x < 69)) ? 2000 + : (((x >= 69) && (x < 100) ? 1900 : 0))); + }); + return "([0-9]+)"; + } + case '%Y': { + fields.push(function(x) { year = x; }); + return "([0-9]+)"; + } + case '%%': { + fields.push(function() {}); + return "%"; + } + } + return s; + }); + + var match = s.match(re); + if (match) match.forEach(function(m, i) { fields[i](m); }); + return new Date(year, month, date, hour, minute, second); + }; + + return format; +}; +/** + * Returns a time format of the given type, either "short" or "long". + * + * @class Represents a time format, converting between a <tt>number</tt> + * representing a duration in milliseconds, and a <tt>string</tt>. Two types of + * time formats are supported: "short" and "long". The <i>short</i> format type + * returns a string such as "3.3 days" or "12.1 minutes", while the <i>long</i> + * format returns "13:04:12" or similar. + * + * @extends pv.Format + * @param {string} type the type; "short" or "long". + */ +pv.Format.time = function(type) { + var pad = pv.Format.pad; + + /* + * MILLISECONDS = 1 + * SECONDS = 1e3 + * MINUTES = 6e4 + * HOURS = 36e5 + * DAYS = 864e5 + * WEEKS = 6048e5 + * MONTHS = 2592e6 + * YEARS = 31536e6 + */ + + /** @private */ + function format(t) { + t = Number(t); // force conversion from Date + switch (type) { + case "short": { + if (t >= 31536e6) { + return (t / 31536e6).toFixed(1) + " years"; + } else if (t >= 6048e5) { + return (t / 6048e5).toFixed(1) + " weeks"; + } else if (t >= 864e5) { + return (t / 864e5).toFixed(1) + " days"; + } else if (t >= 36e5) { + return (t / 36e5).toFixed(1) + " hours"; + } else if (t >= 6e4) { + return (t / 6e4).toFixed(1) + " minutes"; + } + return (t / 1e3).toFixed(1) + " seconds"; + } + case "long": { + var a = [], + s = ((t % 6e4) / 1e3) >> 0, + m = ((t % 36e5) / 6e4) >> 0; + a.push(pad("0", 2, s)); + if (t >= 36e5) { + var h = ((t % 864e5) / 36e5) >> 0; + a.push(pad("0", 2, m)); + if (t >= 864e5) { + a.push(pad("0", 2, h)); + a.push(Math.floor(t / 864e5).toFixed()); + } else { + a.push(h.toFixed()); + } + } else { + a.push(m.toFixed()); + } + return a.reverse().join(":"); + } + } + } + + /** + * Formats the specified time, returning the string representation. + * + * @function + * @name pv.Format.time.prototype.format + * @param {number} t the duration in milliseconds. May also be a <tt>Date</tt>. + * @returns {string} the formatted string. + */ + format.format = format; + + /** + * Parses the specified string, returning the time in milliseconds. + * + * @function + * @name pv.Format.time.prototype.parse + * @param {string} s a formatted string. + * @returns {number} the parsed duration in milliseconds. + */ + format.parse = function(s) { + switch (type) { + case "short": { + var re = /([0-9,.]+)\s*([a-z]+)/g, a, t = 0; + while (a = re.exec(s)) { + var f = parseFloat(a[0].replace(",", "")), u = 0; + switch (a[2].toLowerCase()) { + case "year": case "years": u = 31536e6; break; + case "week": case "weeks": u = 6048e5; break; + case "day": case "days": u = 864e5; break; + case "hour": case "hours": u = 36e5; break; + case "minute": case "minutes": u = 6e4; break; + case "second": case "seconds": u = 1e3; break; + } + t += f * u; + } + return t; + } + case "long": { + var a = s.replace(",", "").split(":").reverse(), t = 0; + if (a.length) t += parseFloat(a[0]) * 1e3; + if (a.length > 1) t += parseFloat(a[1]) * 6e4; + if (a.length > 2) t += parseFloat(a[2]) * 36e5; + if (a.length > 3) t += parseFloat(a[3]) * 864e5; + return t; + } + } + } + + return format; +}; +/** + * Returns a default number format. + * + * @class Represents a number format, converting between a <tt>number</tt> and a + * <tt>string</tt>. This class allows numbers to be formatted with variable + * precision (both for the integral and fractional part of the number), optional + * thousands grouping, and optional padding. The thousands (",") and decimal + * (".") separator can be customized. + * + * @returns {pv.Format.number} a number format. + */ +pv.Format.number = function() { + var mini = 0, // default minimum integer digits + maxi = Infinity, // default maximum integer digits + mins = 0, // mini, including group separators + minf = 0, // default minimum fraction digits + maxf = 0, // default maximum fraction digits + maxk = 1, // 10^maxf + padi = "0", // default integer pad + padf = "0", // default fraction pad + padg = true, // whether group separator affects integer padding + decimal = ".", // default decimal separator + group = ",", // default group separator + np = "\u2212", // default negative prefix + ns = ""; // default negative suffix + + /** @private */ + function format(x) { + /* Round the fractional part, and split on decimal separator. */ + if (Infinity > maxf) x = Math.round(x * maxk) / maxk; + var s = String(Math.abs(x)).split("."); + + /* Pad, truncate and group the integral part. */ + var i = s[0]; + if (i.length > maxi) i = i.substring(i.length - maxi); + if (padg && (i.length < mini)) i = new Array(mini - i.length + 1).join(padi) + i; + if (i.length > 3) i = i.replace(/\B(?=(?:\d{3})+(?!\d))/g, group); + if (!padg && (i.length < mins)) i = new Array(mins - i.length + 1).join(padi) + i; + s[0] = x < 0 ? np + i + ns : i; + + /* Pad the fractional part. */ + var f = s[1] || ""; + if (f.length < minf) s[1] = f + new Array(minf - f.length + 1).join(padf); + + return s.join(decimal); + } + + /** + * @function + * @name pv.Format.number.prototype.format + * @param {number} x + * @returns {string} + */ + format.format = format; + + /** + * Parses the specified string as a number. Before parsing, leading and + * trailing padding is removed. Group separators are also removed, and the + * decimal separator is replaced with the standard point ("."). The integer + * part is truncated per the maximum integer digits, and the fraction part is + * rounded per the maximum fraction digits. + * + * @function + * @name pv.Format.number.prototype.parse + * @param {string} x the string to parse. + * @returns {number} the parsed number. + */ + format.parse = function(x) { + var re = pv.Format.re; + + /* Remove leading and trailing padding. Split on the decimal separator. */ + var s = String(x) + .replace(new RegExp("^(" + re(padi) + ")*"), "") + .replace(new RegExp("(" + re(padf) + ")*$"), "") + .split(decimal); + + /* Remove grouping and truncate the integral part. */ + var i = s[0].replace(new RegExp(re(group), "g"), ""); + if (i.length > maxi) i = i.substring(i.length - maxi); + + /* Round the fractional part. */ + var f = s[1] ? Number("0." + s[1]) : 0; + if (Infinity > maxf) f = Math.round(f * maxk) / maxk; + + return Math.round(i) + f; + }; + + /** + * Sets or gets the minimum and maximum number of integer digits. This + * controls the number of decimal digits to display before the decimal + * separator for the integral part of the number. If the number of digits is + * smaller than the minimum, the digits are padded; if the number of digits is + * larger, the digits are truncated, showing only the lower-order digits. The + * default range is [0, Infinity]. + * + * <p>If only one argument is specified to this method, this value is used as + * both the minimum and maximum number. If no arguments are specified, a + * two-element array is returned containing the minimum and the maximum. + * + * @function + * @name pv.Format.number.prototype.integerDigits + * @param {number} [min] the minimum integer digits. + * @param {number} [max] the maximum integer digits. + * @returns {pv.Format.number} <tt>this</tt>, or the current integer digits. + */ + format.integerDigits = function(min, max) { + if (arguments.length) { + mini = Number(min); + maxi = (arguments.length > 1) ? Number(max) : mini; + mins = mini + Math.floor(mini / 3) * group.length; + return this; + } + return [mini, maxi]; + }; + + /** + * Sets or gets the minimum and maximum number of fraction digits. The + * controls the number of decimal digits to display after the decimal + * separator for the fractional part of the number. If the number of digits is + * smaller than the minimum, the digits are padded; if the number of digits is + * larger, the fractional part is rounded, showing only the higher-order + * digits. The default range is [0, 0]. + * + * <p>If only one argument is specified to this method, this value is used as + * both the minimum and maximum number. If no arguments are specified, a + * two-element array is returned containing the minimum and the maximum. + * + * @function + * @name pv.Format.number.prototype.fractionDigits + * @param {number} [min] the minimum fraction digits. + * @param {number} [max] the maximum fraction digits. + * @returns {pv.Format.number} <tt>this</tt>, or the current fraction digits. + */ + format.fractionDigits = function(min, max) { + if (arguments.length) { + minf = Number(min); + maxf = (arguments.length > 1) ? Number(max) : minf; + maxk = Math.pow(10, maxf); + return this; + } + return [minf, maxf]; + }; + + /** + * Sets or gets the character used to pad the integer part. The integer pad is + * used when the number of integer digits is smaller than the minimum. The + * default pad character is "0" (zero). + * + * @param {string} [x] the new pad character. + * @returns {pv.Format.number} <tt>this</tt> or the current pad character. + */ + format.integerPad = function(x) { + if (arguments.length) { + padi = String(x); + padg = /\d/.test(padi); + return this; + } + return padi; + }; + + /** + * Sets or gets the character used to pad the fration part. The fraction pad + * is used when the number of fraction digits is smaller than the minimum. The + * default pad character is "0" (zero). + * + * @param {string} [x] the new pad character. + * @returns {pv.Format.number} <tt>this</tt> or the current pad character. + */ + format.fractionPad = function(x) { + if (arguments.length) { + padf = String(x); + return this; + } + return padf; + }; + + /** + * Sets or gets the character used as the decimal point, separating the + * integer and fraction parts of the number. The default decimal point is ".". + * + * @param {string} [x] the new decimal separator. + * @returns {pv.Format.number} <tt>this</tt> or the current decimal separator. + */ + format.decimal = function(x) { + if (arguments.length) { + decimal = String(x); + return this; + } + return decimal; + }; + + /** + * Sets or gets the character used as the group separator, grouping integer + * digits by thousands. The default decimal point is ",". Grouping can be + * disabled by using "" for the separator. + * + * @param {string} [x] the new group separator. + * @returns {pv.Format.number} <tt>this</tt> or the current group separator. + */ + format.group = function(x) { + if (arguments.length) { + group = x ? String(x) : ""; + mins = mini + Math.floor(mini / 3) * group.length; + return this; + } + return group; + }; + + /** + * Sets or gets the negative prefix and suffix. The default negative prefix is + * "−", and the default negative suffix is the empty string. + * + * @param {string} [x] the negative prefix. + * @param {string} [y] the negative suffix. + * @returns {pv.Format.number} <tt>this</tt> or the current negative format. + */ + format.negativeAffix = function(x, y) { + if (arguments.length) { + np = String(x || ""); + ns = String(y || ""); + return this; + } + return [np, ns]; + }; + + return format; +}; +/** + * @private A private variant of Array.prototype.map that supports the index + * property. + */ +pv.map = function(array, f) { + var o = {}; + return f + ? array.map(function(d, i) { o.index = i; return f.call(o, d); }) + : array.slice(); +}; + +/** + * Concatenates the specified array with itself <i>n</i> times. For example, + * <tt>pv.repeat([1, 2])</tt> returns [1, 2, 1, 2]. + * + * @param {array} a an array. + * @param {number} [n] the number of times to repeat; defaults to two. + * @returns {array} an array that repeats the specified array. + */ +pv.repeat = function(array, n) { + if (arguments.length == 1) n = 2; + return pv.blend(pv.range(n).map(function() { return array; })); +}; + +/** + * Given two arrays <tt>a</tt> and <tt>b</tt>, <style + * type="text/css">sub{line-height:0}</style> returns an array of all possible + * pairs of elements [a<sub>i</sub>, b<sub>j</sub>]. The outer loop is on array + * <i>a</i>, while the inner loop is on <i>b</i>, such that the order of + * returned elements is [a<sub>0</sub>, b<sub>0</sub>], [a<sub>0</sub>, + * b<sub>1</sub>], ... [a<sub>0</sub>, b<sub>m</sub>], [a<sub>1</sub>, + * b<sub>0</sub>], [a<sub>1</sub>, b<sub>1</sub>], ... [a<sub>1</sub>, + * b<sub>m</sub>], ... [a<sub>n</sub>, b<sub>m</sub>]. If either array is empty, + * an empty array is returned. + * + * @param {array} a an array. + * @param {array} b an array. + * @returns {array} an array of pairs of elements in <tt>a</tt> and <tt>b</tt>. + */ +pv.cross = function(a, b) { + var array = []; + for (var i = 0, n = a.length, m = b.length; i < n; i++) { + for (var j = 0, x = a[i]; j < m; j++) { + array.push([x, b[j]]); + } + } + return array; +}; + +/** + * Given the specified array of arrays, concatenates the arrays into a single + * array. If the individual arrays are explicitly known, an alternative to blend + * is to use JavaScript's <tt>concat</tt> method directly. These two equivalent + * expressions:<ul> + * + * <li><tt>pv.blend([[1, 2, 3], ["a", "b", "c"]])</tt> + * <li><tt>[1, 2, 3].concat(["a", "b", "c"])</tt> + * + * </ul>return [1, 2, 3, "a", "b", "c"]. + * + * @param {array[]} arrays an array of arrays. + * @returns {array} an array containing all the elements of each array in + * <tt>arrays</tt>. + */ +pv.blend = function(arrays) { + return Array.prototype.concat.apply([], arrays); +}; + +/** + * Given the specified array of arrays, <style + * type="text/css">sub{line-height:0}</style> transposes each element + * array<sub>ij</sub> with array<sub>ji</sub>. If the array has dimensions + * <i>n</i>×<i>m</i>, it will have dimensions <i>m</i>×<i>n</i> + * after this method returns. This method transposes the elements of the array + * in place, mutating the array, and returning a reference to the array. + * + * @param {array[]} arrays an array of arrays. + * @returns {array[]} the passed-in array, after transposing the elements. + */ +pv.transpose = function(arrays) { + var n = arrays.length, m = pv.max(arrays, function(d) { return d.length; }); + + if (m > n) { + arrays.length = m; + for (var i = n; i < m; i++) { + arrays[i] = new Array(n); + } + for (var i = 0; i < n; i++) { + for (var j = i + 1; j < m; j++) { + var t = arrays[i][j]; + arrays[i][j] = arrays[j][i]; + arrays[j][i] = t; + } + } + } else { + for (var i = 0; i < m; i++) { + arrays[i].length = n; + } + for (var i = 0; i < n; i++) { + for (var j = 0; j < i; j++) { + var t = arrays[i][j]; + arrays[i][j] = arrays[j][i]; + arrays[j][i] = t; + } + } + } + + arrays.length = m; + for (var i = 0; i < m; i++) { + arrays[i].length = n; + } + + return arrays; +}; + +/** + * Returns a normalized copy of the specified array, such that the sum of the + * returned elements sum to one. If the specified array is not an array of + * numbers, an optional accessor function <tt>f</tt> can be specified to map the + * elements to numbers. For example, if <tt>array</tt> is an array of objects, + * and each object has a numeric property "foo", the expression + * + * <pre>pv.normalize(array, function(d) d.foo)</pre> + * + * returns a normalized array on the "foo" property. If an accessor function is + * not specified, the identity function is used. Accessor functions can refer to + * <tt>this.index</tt>. + * + * @param {array} array an array of objects, or numbers. + * @param {function} [f] an optional accessor function. + * @returns {number[]} an array of numbers that sums to one. + */ +pv.normalize = function(array, f) { + var norm = pv.map(array, f), sum = pv.sum(norm); + for (var i = 0; i < norm.length; i++) norm[i] /= sum; + return norm; +}; + +/** + * Returns a permutation of the specified array, using the specified array of + * indexes. The returned array contains the corresponding element in + * <tt>array</tt> for each index in <tt>indexes</tt>, in order. For example, + * + * <pre>pv.permute(["a", "b", "c"], [1, 2, 0])</pre> + * + * returns <tt>["b", "c", "a"]</tt>. It is acceptable for the array of indexes + * to be a different length from the array of elements, and for indexes to be + * duplicated or omitted. The optional accessor function <tt>f</tt> can be used + * to perform a simultaneous mapping of the array elements. Accessor functions + * can refer to <tt>this.index</tt>. + * + * @param {array} array an array. + * @param {number[]} indexes an array of indexes into <tt>array</tt>. + * @param {function} [f] an optional accessor function. + * @returns {array} an array of elements from <tt>array</tt>; a permutation. + */ +pv.permute = function(array, indexes, f) { + if (!f) f = pv.identity; + var p = new Array(indexes.length), o = {}; + indexes.forEach(function(j, i) { o.index = j; p[i] = f.call(o, array[j]); }); + return p; +}; + +/** + * Returns a map from key to index for the specified <tt>keys</tt> array. For + * example, + * + * <pre>pv.numerate(["a", "b", "c"])</pre> + * + * returns <tt>{a: 0, b: 1, c: 2}</tt>. Note that since JavaScript maps only + * support string keys, <tt>keys</tt> must contain strings, or other values that + * naturally map to distinct string values. Alternatively, an optional accessor + * function <tt>f</tt> can be specified to compute the string key for the given + * element. Accessor functions can refer to <tt>this.index</tt>. + * + * @param {array} keys an array, usually of string keys. + * @param {function} [f] an optional key function. + * @returns a map from key to index. + */ +pv.numerate = function(keys, f) { + if (!f) f = pv.identity; + var map = {}, o = {}; + keys.forEach(function(x, i) { o.index = i; map[f.call(o, x)] = i; }); + return map; +}; + +/** + * Returns the unique elements in the specified array, in the order they appear. + * Note that since JavaScript maps only support string keys, <tt>array</tt> must + * contain strings, or other values that naturally map to distinct string + * values. Alternatively, an optional accessor function <tt>f</tt> can be + * specified to compute the string key for the given element. Accessor functions + * can refer to <tt>this.index</tt>. + * + * @param {array} array an array, usually of string keys. + * @param {function} [f] an optional key function. + * @returns {array} the unique values. + */ +pv.uniq = function(array, f) { + if (!f) f = pv.identity; + var map = {}, keys = [], o = {}, y; + array.forEach(function(x, i) { + o.index = i; + y = f.call(o, x); + if (!(y in map)) map[y] = keys.push(y); + }); + return keys; +}; + +/** + * The comparator function for natural order. This can be used in conjunction with + * the built-in array <tt>sort</tt> method to sort elements by their natural + * order, ascending. Note that if no comparator function is specified to the + * built-in <tt>sort</tt> method, the default order is lexicographic, <i>not</i> + * natural! + * + * @see <a + * href="http://developer.mozilla.org/en/Core_JavaScript_1.5_Reference/Global_Objects/Array/sort">Array.sort</a>. + * @param a an element to compare. + * @param b an element to compare. + * @returns {number} negative if a < b; positive if a > b; otherwise 0. + */ +pv.naturalOrder = function(a, b) { + return (a < b) ? -1 : ((a > b) ? 1 : 0); +}; + +/** + * The comparator function for reverse natural order. This can be used in + * conjunction with the built-in array <tt>sort</tt> method to sort elements by + * their natural order, descending. Note that if no comparator function is + * specified to the built-in <tt>sort</tt> method, the default order is + * lexicographic, <i>not</i> natural! + * + * @see #naturalOrder + * @param a an element to compare. + * @param b an element to compare. + * @returns {number} negative if a < b; positive if a > b; otherwise 0. + */ +pv.reverseOrder = function(b, a) { + return (a < b) ? -1 : ((a > b) ? 1 : 0); +}; + +/** + * Searches the specified array of numbers for the specified value using the + * binary search algorithm. The array must be sorted (as by the <tt>sort</tt> + * method) prior to making this call. If it is not sorted, the results are + * undefined. If the array contains multiple elements with the specified value, + * there is no guarantee which one will be found. + * + * <p>The <i>insertion point</i> is defined as the point at which the value + * would be inserted into the array: the index of the first element greater than + * the value, or <tt>array.length</tt>, if all elements in the array are less + * than the specified value. Note that this guarantees that the return value + * will be nonnegative if and only if the value is found. + * + * @param {number[]} array the array to be searched. + * @param {number} value the value to be searched for. + * @returns the index of the search value, if it is contained in the array; + * otherwise, (-(<i>insertion point</i>) - 1). + * @param {function} [f] an optional key function. + */ +pv.search = function(array, value, f) { + if (!f) f = pv.identity; + var low = 0, high = array.length - 1; + while (low <= high) { + var mid = (low + high) >> 1, midValue = f(array[mid]); + if (midValue < value) low = mid + 1; + else if (midValue > value) high = mid - 1; + else return mid; + } + return -low - 1; +}; + +pv.search.index = function(array, value, f) { + var i = pv.search(array, value, f); + return (i < 0) ? (-i - 1) : i; +}; +/** + * Returns an array of numbers, starting at <tt>start</tt>, incrementing by + * <tt>step</tt>, until <tt>stop</tt> is reached. The stop value is + * exclusive. If only a single argument is specified, this value is interpeted + * as the <i>stop</i> value, with the <i>start</i> value as zero. If only two + * arguments are specified, the step value is implied to be one. + * + * <p>The method is modeled after the built-in <tt>range</tt> method from + * Python. See the Python documentation for more details. + * + * @see <a href="http://docs.python.org/library/functions.html#range">Python range</a> + * @param {number} [start] the start value. + * @param {number} stop the stop value. + * @param {number} [step] the step value. + * @returns {number[]} an array of numbers. + */ +pv.range = function(start, stop, step) { + if (arguments.length == 1) { + stop = start; + start = 0; + } + if (step == undefined) step = 1; + if ((stop - start) / step == Infinity) throw new Error("range must be finite"); + var array = [], i = 0, j; + stop -= (stop - start) * 1e-10; // floating point precision! + if (step < 0) { + while ((j = start + step * i++) > stop) { + array.push(j); + } + } else { + while ((j = start + step * i++) < stop) { + array.push(j); + } + } + return array; +}; + +/** + * Returns a random number in the range [<tt>start</tt>, <tt>stop</tt>) that is + * a multiple of <tt>step</tt>. More specifically, the returned number is of the + * form <tt>start</tt> + <i>n</i> * <tt>step</tt>, where <i>n</i> is a + * nonnegative integer. If <tt>step</tt> is not specified, it defaults to 1, + * returning a random integer if <tt>start</tt> is also an integer. + * + * @param {number} [start] the start value value. + * @param {number} stop the stop value. + * @param {number} [step] the step value. + * @returns {number} a random number between <i>start</i> and <i>stop</i>. + */ +pv.random = function(start, stop, step) { + if (arguments.length == 1) { + stop = start; + start = 0; + } + if (step == undefined) step = 1; + return step + ? (Math.floor(Math.random() * (stop - start) / step) * step + start) + : (Math.random() * (stop - start) + start); +}; + +/** + * Returns the sum of the specified array. If the specified array is not an + * array of numbers, an optional accessor function <tt>f</tt> can be specified + * to map the elements to numbers. See {@link #normalize} for an example. + * Accessor functions can refer to <tt>this.index</tt>. + * + * @param {array} array an array of objects, or numbers. + * @param {function} [f] an optional accessor function. + * @returns {number} the sum of the specified array. + */ +pv.sum = function(array, f) { + var o = {}; + return array.reduce(f + ? function(p, d, i) { o.index = i; return p + f.call(o, d); } + : function(p, d) { return p + d; }, 0); +}; + +/** + * Returns the maximum value of the specified array. If the specified array is + * not an array of numbers, an optional accessor function <tt>f</tt> can be + * specified to map the elements to numbers. See {@link #normalize} for an + * example. Accessor functions can refer to <tt>this.index</tt>. + * + * @param {array} array an array of objects, or numbers. + * @param {function} [f] an optional accessor function. + * @returns {number} the maximum value of the specified array. + */ +pv.max = function(array, f) { + if (f == pv.index) return array.length - 1; + return Math.max.apply(null, f ? pv.map(array, f) : array); +}; + +/** + * Returns the index of the maximum value of the specified array. If the + * specified array is not an array of numbers, an optional accessor function + * <tt>f</tt> can be specified to map the elements to numbers. See + * {@link #normalize} for an example. Accessor functions can refer to + * <tt>this.index</tt>. + * + * @param {array} array an array of objects, or numbers. + * @param {function} [f] an optional accessor function. + * @returns {number} the index of the maximum value of the specified array. + */ +pv.max.index = function(array, f) { + if (!array.length) return -1; + if (f == pv.index) return array.length - 1; + if (!f) f = pv.identity; + var maxi = 0, maxx = -Infinity, o = {}; + for (var i = 0; i < array.length; i++) { + o.index = i; + var x = f.call(o, array[i]); + if (x > maxx) { + maxx = x; + maxi = i; + } + } + return maxi; +} + +/** + * Returns the minimum value of the specified array of numbers. If the specified + * array is not an array of numbers, an optional accessor function <tt>f</tt> + * can be specified to map the elements to numbers. See {@link #normalize} for + * an example. Accessor functions can refer to <tt>this.index</tt>. + * + * @param {array} array an array of objects, or numbers. + * @param {function} [f] an optional accessor function. + * @returns {number} the minimum value of the specified array. + */ +pv.min = function(array, f) { + if (f == pv.index) return 0; + return Math.min.apply(null, f ? pv.map(array, f) : array); +}; + +/** + * Returns the index of the minimum value of the specified array. If the + * specified array is not an array of numbers, an optional accessor function + * <tt>f</tt> can be specified to map the elements to numbers. See + * {@link #normalize} for an example. Accessor functions can refer to + * <tt>this.index</tt>. + * + * @param {array} array an array of objects, or numbers. + * @param {function} [f] an optional accessor function. + * @returns {number} the index of the minimum value of the specified array. + */ +pv.min.index = function(array, f) { + if (!array.length) return -1; + if (f == pv.index) return 0; + if (!f) f = pv.identity; + var mini = 0, minx = Infinity, o = {}; + for (var i = 0; i < array.length; i++) { + o.index = i; + var x = f.call(o, array[i]); + if (x < minx) { + minx = x; + mini = i; + } + } + return mini; +} + +/** + * Returns the arithmetic mean, or average, of the specified array. If the + * specified array is not an array of numbers, an optional accessor function + * <tt>f</tt> can be specified to map the elements to numbers. See + * {@link #normalize} for an example. Accessor functions can refer to + * <tt>this.index</tt>. + * + * @param {array} array an array of objects, or numbers. + * @param {function} [f] an optional accessor function. + * @returns {number} the mean of the specified array. + */ +pv.mean = function(array, f) { + return pv.sum(array, f) / array.length; +}; + +/** + * Returns the median of the specified array. If the specified array is not an + * array of numbers, an optional accessor function <tt>f</tt> can be specified + * to map the elements to numbers. See {@link #normalize} for an example. + * Accessor functions can refer to <tt>this.index</tt>. + * + * @param {array} array an array of objects, or numbers. + * @param {function} [f] an optional accessor function. + * @returns {number} the median of the specified array. + */ +pv.median = function(array, f) { + if (f == pv.index) return (array.length - 1) / 2; + array = pv.map(array, f).sort(pv.naturalOrder); + if (array.length % 2) return array[Math.floor(array.length / 2)]; + var i = array.length / 2; + return (array[i - 1] + array[i]) / 2; +}; + +/** + * Returns the unweighted variance of the specified array. If the specified + * array is not an array of numbers, an optional accessor function <tt>f</tt> + * can be specified to map the elements to numbers. See {@link #normalize} for + * an example. Accessor functions can refer to <tt>this.index</tt>. + * + * @param {array} array an array of objects, or numbers. + * @param {function} [f] an optional accessor function. + * @returns {number} the variance of the specified array. + */ +pv.variance = function(array, f) { + if (array.length < 1) return NaN; + if (array.length == 1) return 0; + var mean = pv.mean(array, f), sum = 0, o = {}; + if (!f) f = pv.identity; + for (var i = 0; i < array.length; i++) { + o.index = i; + var d = f.call(o, array[i]) - mean; + sum += d * d; + } + return sum; +}; + +/** + * Returns an unbiased estimation of the standard deviation of a population, + * given the specified random sample. If the specified array is not an array of + * numbers, an optional accessor function <tt>f</tt> can be specified to map the + * elements to numbers. See {@link #normalize} for an example. Accessor + * functions can refer to <tt>this.index</tt>. + * + * @param {array} array an array of objects, or numbers. + * @param {function} [f] an optional accessor function. + * @returns {number} the standard deviation of the specified array. + */ +pv.deviation = function(array, f) { + return Math.sqrt(pv.variance(array, f) / (array.length - 1)); +}; + +/** + * Returns the logarithm with a given base value. + * + * @param {number} x the number for which to compute the logarithm. + * @param {number} b the base of the logarithm. + * @returns {number} the logarithm value. + */ +pv.log = function(x, b) { + return Math.log(x) / Math.log(b); +}; + +/** + * Computes a zero-symmetric logarithm. Computes the logarithm of the absolute + * value of the input, and determines the sign of the output according to the + * sign of the input value. + * + * @param {number} x the number for which to compute the logarithm. + * @param {number} b the base of the logarithm. + * @returns {number} the symmetric log value. + */ +pv.logSymmetric = function(x, b) { + return (x == 0) ? 0 : ((x < 0) ? -pv.log(-x, b) : pv.log(x, b)); +}; + +/** + * Computes a zero-symmetric logarithm, with adjustment to values between zero + * and the logarithm base. This adjustment introduces distortion for values less + * than the base number, but enables simultaneous plotting of log-transformed + * data involving both positive and negative numbers. + * + * @param {number} x the number for which to compute the logarithm. + * @param {number} b the base of the logarithm. + * @returns {number} the adjusted, symmetric log value. + */ +pv.logAdjusted = function(x, b) { + if (!isFinite(x)) return x; + var negative = x < 0; + if (x < b) x += (b - x) / b; + return negative ? -pv.log(x, b) : pv.log(x, b); +}; + +/** + * Rounds an input value down according to its logarithm. The method takes the + * floor of the logarithm of the value and then uses the resulting value as an + * exponent for the base value. + * + * @param {number} x the number for which to compute the logarithm floor. + * @param {number} b the base of the logarithm. + * @returns {number} the rounded-by-logarithm value. + */ +pv.logFloor = function(x, b) { + return (x > 0) + ? Math.pow(b, Math.floor(pv.log(x, b))) + : -Math.pow(b, -Math.floor(-pv.log(-x, b))); +}; + +/** + * Rounds an input value up according to its logarithm. The method takes the + * ceiling of the logarithm of the value and then uses the resulting value as an + * exponent for the base value. + * + * @param {number} x the number for which to compute the logarithm ceiling. + * @param {number} b the base of the logarithm. + * @returns {number} the rounded-by-logarithm value. + */ +pv.logCeil = function(x, b) { + return (x > 0) + ? Math.pow(b, Math.ceil(pv.log(x, b))) + : -Math.pow(b, -Math.ceil(-pv.log(-x, b))); +}; + +(function() { + var radians = Math.PI / 180, + degrees = 180 / Math.PI; + + /** Returns the number of radians corresponding to the specified degrees. */ + pv.radians = function(degrees) { return radians * degrees; }; + + /** Returns the number of degrees corresponding to the specified radians. */ + pv.degrees = function(radians) { return degrees * radians; }; +})(); +/** + * Returns all of the property names (keys) of the specified object (a map). The + * order of the returned array is not defined. + * + * @param map an object. + * @returns {string[]} an array of strings corresponding to the keys. + * @see #entries + */ +pv.keys = function(map) { + var array = []; + for (var key in map) { + array.push(key); + } + return array; +}; + +/** + * Returns all of the entries (key-value pairs) of the specified object (a + * map). The order of the returned array is not defined. Each key-value pair is + * represented as an object with <tt>key</tt> and <tt>value</tt> attributes, + * e.g., <tt>{key: "foo", value: 42}</tt>. + * + * @param map an object. + * @returns {array} an array of key-value pairs corresponding to the keys. + */ +pv.entries = function(map) { + var array = []; + for (var key in map) { + array.push({ key: key, value: map[key] }); + } + return array; +}; + +/** + * Returns all of the values (attribute values) of the specified object (a + * map). The order of the returned array is not defined. + * + * @param map an object. + * @returns {array} an array of objects corresponding to the values. + * @see #entries + */ +pv.values = function(map) { + var array = []; + for (var key in map) { + array.push(map[key]); + } + return array; +}; + +/** + * Returns a map constructed from the specified <tt>keys</tt>, using the + * function <tt>f</tt> to compute the value for each key. The single argument to + * the value function is the key. The callback is invoked only for indexes of + * the array which have assigned values; it is not invoked for indexes which + * have been deleted or which have never been assigned values. + * + * <p>For example, this expression creates a map from strings to string length: + * + * <pre>pv.dict(["one", "three", "seventeen"], function(s) s.length)</pre> + * + * The returned value is <tt>{one: 3, three: 5, seventeen: 9}</tt>. Accessor + * functions can refer to <tt>this.index</tt>. + * + * @param {array} keys an array. + * @param {function} f a value function. + * @returns a map from keys to values. + */ +pv.dict = function(keys, f) { + var m = {}, o = {}; + for (var i = 0; i < keys.length; i++) { + if (i in keys) { + var k = keys[i]; + o.index = i; + m[k] = f.call(o, k); + } + } + return m; +}; +/** + * Returns a {@link pv.Dom} operator for the given map. This is a convenience + * factory method, equivalent to <tt>new pv.Dom(map)</tt>. To apply the operator + * and retrieve the root node, call {@link pv.Dom#root}; to retrieve all nodes + * flattened, use {@link pv.Dom#nodes}. + * + * @see pv.Dom + * @param map a map from which to construct a DOM. + * @returns {pv.Dom} a DOM operator for the specified map. + */ +pv.dom = function(map) { + return new pv.Dom(map); +}; + +/** + * Constructs a DOM operator for the specified map. This constructor should not + * be invoked directly; use {@link pv.dom} instead. + * + * @class Represets a DOM operator for the specified map. This allows easy + * transformation of a hierarchical JavaScript object (such as a JSON map) to a + * W3C Document Object Model hierarchy. For more information on which attributes + * and methods from the specification are supported, see {@link pv.Dom.Node}. + * + * <p>Leaves in the map are determined using an associated <i>leaf</i> function; + * see {@link #leaf}. By default, leaves are any value whose type is not + * "object", such as numbers or strings. + * + * @param map a map from which to construct a DOM. + */ +pv.Dom = function(map) { + this.$map = map; +}; + +/** @private The default leaf function. */ +pv.Dom.prototype.$leaf = function(n) { + return typeof n != "object"; +}; + +/** + * Sets or gets the leaf function for this DOM operator. The leaf function + * identifies which values in the map are leaves, and which are internal nodes. + * By default, objects are considered internal nodes, and primitives (such as + * numbers and strings) are considered leaves. + * + * @param {function} f the new leaf function. + * @returns the current leaf function, or <tt>this</tt>. + */ +pv.Dom.prototype.leaf = function(f) { + if (arguments.length) { + this.$leaf = f; + return this; + } + return this.$leaf; +}; + +/** + * Applies the DOM operator, returning the root node. + * + * @returns {pv.Dom.Node} the root node. + * @param {string} [nodeName] optional node name for the root. + */ +pv.Dom.prototype.root = function(nodeName) { + var leaf = this.$leaf, root = recurse(this.$map); + + /** @private */ + function recurse(map) { + var n = new pv.Dom.Node(); + for (var k in map) { + var v = map[k]; + n.appendChild(leaf(v) ? new pv.Dom.Node(v) : recurse(v)).nodeName = k; + } + return n; + } + + root.nodeName = nodeName; + return root; +}; + +/** + * Applies the DOM operator, returning the array of all nodes in preorder + * traversal. + * + * @returns {array} the array of nodes in preorder traversal. + */ +pv.Dom.prototype.nodes = function() { + return this.root().nodes(); +}; + +/** + * Constructs a DOM node for the specified value. Instances of this class are + * not typically created directly; instead they are generated from a JavaScript + * map using the {@link pv.Dom} operator. + * + * @class Represents a <tt>Node</tt> in the W3C Document Object Model. + */ +pv.Dom.Node = function(value) { + this.nodeValue = value; + this.childNodes = []; +}; + +/** + * The node name. When generated from a map, the node name corresponds to the + * key at the given level in the map. Note that the root node has no associated + * key, and thus has an undefined node name (and no <tt>parentNode</tt>). + * + * @type string + * @field pv.Dom.Node.prototype.nodeName + */ + +/** + * The node value. When generated from a map, node value corresponds to the leaf + * value for leaf nodes, and is undefined for internal nodes. + * + * @field pv.Dom.Node.prototype.nodeValue + */ + +/** + * The array of child nodes. This array is empty for leaf nodes. An easy way to + * check if child nodes exist is to query <tt>firstChild</tt>. + * + * @type array + * @field pv.Dom.Node.prototype.childNodes + */ + +/** + * The parent node, which is null for root nodes. + * + * @type pv.Dom.Node + */ +pv.Dom.Node.prototype.parentNode = null; + +/** + * The first child, which is null for leaf nodes. + * + * @type pv.Dom.Node + */ +pv.Dom.Node.prototype.firstChild = null; + +/** + * The last child, which is null for leaf nodes. + * + * @type pv.Dom.Node + */ +pv.Dom.Node.prototype.lastChild = null; + +/** + * The previous sibling node, which is null for the first child. + * + * @type pv.Dom.Node + */ +pv.Dom.Node.prototype.previousSibling = null; + +/** + * The next sibling node, which is null for the last child. + * + * @type pv.Dom.Node + */ +pv.Dom.Node.prototype.nextSibling = null; + +/** + * Removes the specified child node from this node. + * + * @throws Error if the specified child is not a child of this node. + * @returns {pv.Dom.Node} the removed child. + */ +pv.Dom.Node.prototype.removeChild = function(n) { + var i = this.childNodes.indexOf(n); + if (i == -1) throw new Error("child not found"); + this.childNodes.splice(i, 1); + if (n.previousSibling) n.previousSibling.nextSibling = n.nextSibling; + else this.firstChild = n.nextSibling; + if (n.nextSibling) n.nextSibling.previousSibling = n.previousSibling; + else this.lastChild = n.previousSibling; + delete n.nextSibling; + delete n.previousSibling; + delete n.parentNode; + return n; +}; + +/** + * Appends the specified child node to this node. If the specified child is + * already part of the DOM, the child is first removed before being added to + * this node. + * + * @returns {pv.Dom.Node} the appended child. + */ +pv.Dom.Node.prototype.appendChild = function(n) { + if (n.parentNode) n.parentNode.removeChild(n); + n.parentNode = this; + n.previousSibling = this.lastChild; + if (this.lastChild) this.lastChild.nextSibling = n; + else this.firstChild = n; + this.lastChild = n; + this.childNodes.push(n); + return n; +}; + +/** + * Inserts the specified child <i>n</i> before the given reference child + * <i>r</i> of this node. If <i>r</i> is null, this method is equivalent to + * {@link #appendChild}. If <i>n</i> is already part of the DOM, it is first + * removed before being inserted. + * + * @throws Error if <i>r</i> is non-null and not a child of this node. + * @returns {pv.Dom.Node} the inserted child. + */ +pv.Dom.Node.prototype.insertBefore = function(n, r) { + if (!r) return this.appendChild(n); + var i = this.childNodes.indexOf(r); + if (i == -1) throw new Error("child not found"); + if (n.parentNode) n.parentNode.removeChild(n); + n.parentNode = this; + n.nextSibling = r; + n.previousSibling = r.previousSibling; + if (r.previousSibling) { + r.previousSibling.nextSibling = n; + } else { + if (r == this.lastChild) this.lastChild = n; + this.firstChild = n; + } + this.childNodes.splice(i, 0, n); + return n; +}; + +/** + * Replaces the specified child <i>r</i> of this node with the node <i>n</i>. If + * <i>n</i> is already part of the DOM, it is first removed before being added. + * + * @throws Error if <i>r</i> is not a child of this node. + */ +pv.Dom.Node.prototype.replaceChild = function(n, r) { + var i = this.childNodes.indexOf(r); + if (i == -1) throw new Error("child not found"); + if (n.parentNode) n.parentNode.removeChild(n); + n.parentNode = this; + n.nextSibling = r.nextSibling; + n.previousSibling = r.previousSibling; + if (r.previousSibling) r.previousSibling.nextSibling = n; + else this.firstChild = n; + if (r.nextSibling) r.nextSibling.previousSibling = n; + else this.lastChild = n; + this.childNodes[i] = n; + return r; +}; + +/** + * Visits each node in the tree in preorder traversal, applying the specified + * function <i>f</i>. The arguments to the function are:<ol> + * + * <li>The current node. + * <li>The current depth, starting at 0 for the root node.</ol> + * + * @param {function} f a function to apply to each node. + */ +pv.Dom.Node.prototype.visitBefore = function(f) { + function visit(n, i) { + f(n, i); + for (var c = n.firstChild; c; c = c.nextSibling) { + visit(c, i + 1); + } + } + visit(this, 0); +}; + +/** + * Visits each node in the tree in postorder traversal, applying the specified + * function <i>f</i>. The arguments to the function are:<ol> + * + * <li>The current node. + * <li>The current depth, starting at 0 for the root node.</ol> + * + * @param {function} f a function to apply to each node. + */ +pv.Dom.Node.prototype.visitAfter = function(f) { + function visit(n, i) { + for (var c = n.firstChild; c; c = c.nextSibling) { + visit(c, i + 1); + } + f(n, i); + } + visit(this, 0); +}; + +/** + * Sorts child nodes of this node, and all descendent nodes recursively, using + * the specified comparator function <tt>f</tt>. The comparator function is + * passed two nodes to compare. + * + * <p>Note: during the sort operation, the comparator function should not rely + * on the tree being well-formed; the values of <tt>previousSibling</tt> and + * <tt>nextSibling</tt> for the nodes being compared are not defined during the + * sort operation. + * + * @param {function} f a comparator function. + * @returns this. + */ +pv.Dom.Node.prototype.sort = function(f) { + if (this.firstChild) { + this.childNodes.sort(f); + var p = this.firstChild = this.childNodes[0], c; + delete p.previousSibling; + for (var i = 1; i < this.childNodes.length; i++) { + p.sort(f); + c = this.childNodes[i]; + c.previousSibling = p; + p = p.nextSibling = c; + } + this.lastChild = p; + delete p.nextSibling; + p.sort(f); + } + return this; +}; + +/** + * Reverses all sibling nodes. + * + * @returns this. + */ +pv.Dom.Node.prototype.reverse = function() { + var childNodes = []; + this.visitAfter(function(n) { + while (n.lastChild) childNodes.push(n.removeChild(n.lastChild)); + for (var c; c = childNodes.pop();) n.insertBefore(c, n.firstChild); + }); + return this; +}; + +/** Returns all descendants of this node in preorder traversal. */ +pv.Dom.Node.prototype.nodes = function() { + var array = []; + + /** @private */ + function flatten(node) { + array.push(node); + node.childNodes.forEach(flatten); + } + + flatten(this, array); + return array; +}; + +/** + * Toggles the child nodes of this node. If this node is not yet toggled, this + * method removes all child nodes and appends them to a new <tt>toggled</tt> + * array attribute on this node. Otherwise, if this node is toggled, this method + * re-adds all toggled child nodes and deletes the <tt>toggled</tt> attribute. + * + * <p>This method has no effect if the node has no child nodes. + * + * @param {boolean} [recursive] whether the toggle should apply to descendants. + */ +pv.Dom.Node.prototype.toggle = function(recursive) { + if (recursive) return this.toggled + ? this.visitBefore(function(n) { if (n.toggled) n.toggle(); }) + : this.visitAfter(function(n) { if (!n.toggled) n.toggle(); }); + var n = this; + if (n.toggled) { + for (var c; c = n.toggled.pop();) n.appendChild(c); + delete n.toggled; + } else if (n.lastChild) { + n.toggled = []; + while (n.lastChild) n.toggled.push(n.removeChild(n.lastChild)); + } +}; + +/** + * Given a flat array of values, returns a simple DOM with each value wrapped by + * a node that is a child of the root node. + * + * @param {array} values. + * @returns {array} nodes. + */ +pv.nodes = function(values) { + var root = new pv.Dom.Node(); + for (var i = 0; i < values.length; i++) { + root.appendChild(new pv.Dom.Node(values[i])); + } + return root.nodes(); +}; +/** + * Returns a {@link pv.Tree} operator for the specified array. This is a + * convenience factory method, equivalent to <tt>new pv.Tree(array)</tt>. + * + * @see pv.Tree + * @param {array} array an array from which to construct a tree. + * @returns {pv.Tree} a tree operator for the specified array. + */ +pv.tree = function(array) { + return new pv.Tree(array); +}; + +/** + * Constructs a tree operator for the specified array. This constructor should + * not be invoked directly; use {@link pv.tree} instead. + * + * @class Represents a tree operator for the specified array. The tree operator + * allows a hierarchical map to be constructed from an array; it is similar to + * the {@link pv.Nest} operator, except the hierarchy is derived dynamically + * from the array elements. + * + * <p>For example, given an array of size information for ActionScript classes: + * + * <pre>{ name: "flare.flex.FlareVis", size: 4116 }, + * { name: "flare.physics.DragForce", size: 1082 }, + * { name: "flare.physics.GravityForce", size: 1336 }, ...</pre> + * + * To facilitate visualization, it may be useful to nest the elements by their + * package hierarchy: + * + * <pre>var tree = pv.tree(classes) + * .keys(function(d) d.name.split(".")) + * .map();</pre> + * + * The resulting tree is: + * + * <pre>{ flare: { + * flex: { + * FlareVis: { + * name: "flare.flex.FlareVis", + * size: 4116 } }, + * physics: { + * DragForce: { + * name: "flare.physics.DragForce", + * size: 1082 }, + * GravityForce: { + * name: "flare.physics.GravityForce", + * size: 1336 } }, + * ... } }</pre> + * + * By specifying a value function, + * + * <pre>var tree = pv.tree(classes) + * .keys(function(d) d.name.split(".")) + * .value(function(d) d.size) + * .map();</pre> + * + * we can further eliminate redundant data: + * + * <pre>{ flare: { + * flex: { + * FlareVis: 4116 }, + * physics: { + * DragForce: 1082, + * GravityForce: 1336 }, + * ... } }</pre> + * + * For visualizations with large data sets, performance improvements may be seen + * by storing the data in a tree format, and then flattening it into an array at + * runtime with {@link pv.Flatten}. + * + * @param {array} array an array from which to construct a tree. + */ +pv.Tree = function(array) { + this.array = array; +}; + +/** + * Assigns a <i>keys</i> function to this operator; required. The keys function + * returns an array of <tt>string</tt>s for each element in the associated + * array; these keys determine how the elements are nested in the tree. The + * returned keys should be unique for each element in the array; otherwise, the + * behavior of this operator is undefined. + * + * @param {function} k the keys function. + * @returns {pv.Tree} this. + */ +pv.Tree.prototype.keys = function(k) { + this.k = k; + return this; +}; + +/** + * Assigns a <i>value</i> function to this operator; optional. The value + * function specifies an optional transformation of the element in the array + * before it is inserted into the map. If no value function is specified, it is + * equivalent to using the identity function. + * + * @param {function} k the value function. + * @returns {pv.Tree} this. + */ +pv.Tree.prototype.value = function(v) { + this.v = v; + return this; +}; + +/** + * Returns a hierarchical map of values. The hierarchy is determined by the keys + * function; the values in the map are determined by the value function. + * + * @returns a hierarchical map of values. + */ +pv.Tree.prototype.map = function() { + var map = {}, o = {}; + for (var i = 0; i < this.array.length; i++) { + o.index = i; + var value = this.array[i], keys = this.k.call(o, value), node = map; + for (var j = 0; j < keys.length - 1; j++) { + node = node[keys[j]] || (node[keys[j]] = {}); + } + node[keys[j]] = this.v ? this.v.call(o, value) : value; + } + return map; +}; +/** + * Returns a {@link pv.Nest} operator for the specified array. This is a + * convenience factory method, equivalent to <tt>new pv.Nest(array)</tt>. + * + * @see pv.Nest + * @param {array} array an array of elements to nest. + * @returns {pv.Nest} a nest operator for the specified array. + */ +pv.nest = function(array) { + return new pv.Nest(array); +}; + +/** + * Constructs a nest operator for the specified array. This constructor should + * not be invoked directly; use {@link pv.nest} instead. + * + * @class Represents a {@link Nest} operator for the specified array. Nesting + * allows elements in an array to be grouped into a hierarchical tree + * structure. The levels in the tree are specified by <i>key</i> functions. The + * leaf nodes of the tree can be sorted by value, while the internal nodes can + * be sorted by key. Finally, the tree can be returned either has a + * multidimensional array via {@link #entries}, or as a hierarchical map via + * {@link #map}. The {@link #rollup} routine similarly returns a map, collapsing + * the elements in each leaf node using a summary function. + * + * <p>For example, consider the following tabular data structure of Barley + * yields, from various sites in Minnesota during 1931-2: + * + * <pre>{ yield: 27.00, variety: "Manchuria", year: 1931, site: "University Farm" }, + * { yield: 48.87, variety: "Manchuria", year: 1931, site: "Waseca" }, + * { yield: 27.43, variety: "Manchuria", year: 1931, site: "Morris" }, ...</pre> + * + * To facilitate visualization, it may be useful to nest the elements first by + * year, and then by variety, as follows: + * + * <pre>var nest = pv.nest(yields) + * .key(function(d) d.year) + * .key(function(d) d.variety) + * .entries();</pre> + * + * This returns a nested array. Each element of the outer array is a key-values + * pair, listing the values for each distinct key: + * + * <pre>{ key: 1931, values: [ + * { key: "Manchuria", values: [ + * { yield: 27.00, variety: "Manchuria", year: 1931, site: "University Farm" }, + * { yield: 48.87, variety: "Manchuria", year: 1931, site: "Waseca" }, + * { yield: 27.43, variety: "Manchuria", year: 1931, site: "Morris" }, + * ... + * ] }, + * { key: "Glabron", values: [ + * { yield: 43.07, variety: "Glabron", year: 1931, site: "University Farm" }, + * { yield: 55.20, variety: "Glabron", year: 1931, site: "Waseca" }, + * ... + * ] }, + * ] }, + * { key: 1932, values: ... }</pre> + * + * Further details, including sorting and rollup, is provided below on the + * corresponding methods. + * + * @param {array} array an array of elements to nest. + */ +pv.Nest = function(array) { + this.array = array; + this.keys = []; +}; + +/** + * Nests using the specified key function. Multiple keys may be added to the + * nest; the array elements will be nested in the order keys are specified. + * + * @param {function} key a key function; must return a string or suitable map + * key. + * @returns {pv.Nest} this. + */ +pv.Nest.prototype.key = function(key) { + this.keys.push(key); + return this; +}; + +/** + * Sorts the previously-added keys. The natural sort order is used by default + * (see {@link pv.naturalOrder}); if an alternative order is desired, + * <tt>order</tt> should be a comparator function. If this method is not called + * (i.e., keys are <i>unsorted</i>), keys will appear in the order they appear + * in the underlying elements array. For example, + * + * <pre>pv.nest(yields) + * .key(function(d) d.year) + * .key(function(d) d.variety) + * .sortKeys() + * .entries()</pre> + * + * groups yield data by year, then variety, and sorts the variety groups + * lexicographically (since the variety attribute is a string). + * + * <p>Key sort order is only used in conjunction with {@link #entries}, which + * returns an array of key-values pairs. If the nest is used to construct a + * {@link #map} instead, keys are unsorted. + * + * @param {function} [order] an optional comparator function. + * @returns {pv.Nest} this. + */ +pv.Nest.prototype.sortKeys = function(order) { + this.keys[this.keys.length - 1].order = order || pv.naturalOrder; + return this; +}; + +/** + * Sorts the leaf values. The natural sort order is used by default (see + * {@link pv.naturalOrder}); if an alternative order is desired, <tt>order</tt> + * should be a comparator function. If this method is not called (i.e., values + * are <i>unsorted</i>), values will appear in the order they appear in the + * underlying elements array. For example, + * + * <pre>pv.nest(yields) + * .key(function(d) d.year) + * .key(function(d) d.variety) + * .sortValues(function(a, b) a.yield - b.yield) + * .entries()</pre> + * + * groups yield data by year, then variety, and sorts the values for each + * variety group by yield. + * + * <p>Value sort order, unlike keys, applies to both {@link #entries} and + * {@link #map}. It has no effect on {@link #rollup}. + * + * @param {function} [order] an optional comparator function. + * @returns {pv.Nest} this. + */ +pv.Nest.prototype.sortValues = function(order) { + this.order = order || pv.naturalOrder; + return this; +}; + +/** + * Returns a hierarchical map of values. Each key adds one level to the + * hierarchy. With only a single key, the returned map will have a key for each + * distinct value of the key function; the correspond value with be an array of + * elements with that key value. If a second key is added, this will be a nested + * map. For example: + * + * <pre>pv.nest(yields) + * .key(function(d) d.variety) + * .key(function(d) d.site) + * .map()</pre> + * + * returns a map <tt>m</tt> such that <tt>m[variety][site]</tt> is an array, a subset of + * <tt>yields</tt>, with each element having the given variety and site. + * + * @returns a hierarchical map of values. + */ +pv.Nest.prototype.map = function() { + var map = {}, values = []; + + /* Build the map. */ + for (var i, j = 0; j < this.array.length; j++) { + var x = this.array[j]; + var m = map; + for (i = 0; i < this.keys.length - 1; i++) { + var k = this.keys[i](x); + if (!m[k]) m[k] = {}; + m = m[k]; + } + k = this.keys[i](x); + if (!m[k]) { + var a = []; + values.push(a); + m[k] = a; + } + m[k].push(x); + } + + /* Sort each leaf array. */ + if (this.order) { + for (var i = 0; i < values.length; i++) { + values[i].sort(this.order); + } + } + + return map; +}; + +/** + * Returns a hierarchical nested array. This method is similar to + * {@link pv.entries}, but works recursively on the entire hierarchy. Rather + * than returning a map like {@link #map}, this method returns a nested + * array. Each element of the array has a <tt>key</tt> and <tt>values</tt> + * field. For leaf nodes, the <tt>values</tt> array will be a subset of the + * underlying elements array; for non-leaf nodes, the <tt>values</tt> array will + * contain more key-values pairs. + * + * <p>For an example usage, see the {@link Nest} constructor. + * + * @returns a hierarchical nested array. + */ +pv.Nest.prototype.entries = function() { + + /** Recursively extracts the entries for the given map. */ + function entries(map) { + var array = []; + for (var k in map) { + var v = map[k]; + array.push({ key: k, values: (v instanceof Array) ? v : entries(v) }); + }; + return array; + } + + /** Recursively sorts the values for the given key-values array. */ + function sort(array, i) { + var o = this.keys[i].order; + if (o) array.sort(function(a, b) { return o(a.key, b.key); }); + if (++i < this.keys.length) { + for (var j = 0; j < array.length; j++) { + sort.call(this, array[j].values, i); + } + } + return array; + } + + return sort.call(this, entries(this.map()), 0); +}; + +/** + * Returns a rollup map. The behavior of this method is the same as + * {@link #map}, except that the leaf values are replaced with the return value + * of the specified rollup function <tt>f</tt>. For example, + * + * <pre>pv.nest(yields) + * .key(function(d) d.site) + * .rollup(function(v) pv.median(v, function(d) d.yield))</pre> + * + * first groups yield data by site, and then returns a map from site to median + * yield for the given site. + * + * @see #map + * @param {function} f a rollup function. + * @returns a hierarchical map, with the leaf values computed by <tt>f</tt>. + */ +pv.Nest.prototype.rollup = function(f) { + + /** Recursively descends to the leaf nodes (arrays) and does rollup. */ + function rollup(map) { + for (var key in map) { + var value = map[key]; + if (value instanceof Array) { + map[key] = f(value); + } else { + rollup(value); + } + } + return map; + } + + return rollup(this.map()); +}; +/** + * Returns a {@link pv.Flatten} operator for the specified map. This is a + * convenience factory method, equivalent to <tt>new pv.Flatten(map)</tt>. + * + * @see pv.Flatten + * @param map a map to flatten. + * @returns {pv.Flatten} a flatten operator for the specified map. + */ +pv.flatten = function(map) { + return new pv.Flatten(map); +}; + +/** + * Constructs a flatten operator for the specified map. This constructor should + * not be invoked directly; use {@link pv.flatten} instead. + * + * @class Represents a flatten operator for the specified array. Flattening + * allows hierarchical maps to be flattened into an array. The levels in the + * input tree are specified by <i>key</i> functions. + * + * <p>For example, consider the following hierarchical data structure of Barley + * yields, from various sites in Minnesota during 1931-2: + * + * <pre>{ 1931: { + * Manchuria: { + * "University Farm": 27.00, + * "Waseca": 48.87, + * "Morris": 27.43, + * ... }, + * Glabron: { + * "University Farm": 43.07, + * "Waseca": 55.20, + * ... } }, + * 1932: { + * ... } }</pre> + * + * To facilitate visualization, it may be useful to flatten the tree into a + * tabular array: + * + * <pre>var array = pv.flatten(yields) + * .key("year") + * .key("variety") + * .key("site") + * .key("yield") + * .array();</pre> + * + * This returns an array of object elements. Each element in the array has + * attributes corresponding to this flatten operator's keys: + * + * <pre>{ site: "University Farm", variety: "Manchuria", year: 1931, yield: 27 }, + * { site: "Waseca", variety: "Manchuria", year: 1931, yield: 48.87 }, + * { site: "Morris", variety: "Manchuria", year: 1931, yield: 27.43 }, + * { site: "University Farm", variety: "Glabron", year: 1931, yield: 43.07 }, + * { site: "Waseca", variety: "Glabron", year: 1931, yield: 55.2 }, ...</pre> + * + * <p>The flatten operator is roughly the inverse of the {@link pv.Nest} and + * {@link pv.Tree} operators. + * + * @param map a map to flatten. + */ +pv.Flatten = function(map) { + this.map = map; + this.keys = []; +}; + +/** + * Flattens using the specified key function. Multiple keys may be added to the + * flatten; the tiers of the underlying tree must correspond to the specified + * keys, in order. The order of the returned array is undefined; however, you + * can easily sort it. + * + * @param {string} key the key name. + * @param {function} [f] an optional value map function. + * @returns {pv.Nest} this. + */ +pv.Flatten.prototype.key = function(key, f) { + this.keys.push({name: key, value: f}); + delete this.$leaf; + return this; +}; + +/** + * Flattens using the specified leaf function. This is an alternative to + * specifying an explicit set of keys; the tiers of the underlying tree will be + * determined dynamically by recursing on the values, and the resulting keys + * will be stored in the entries <tt>keys</tt> attribute. The leaf function must + * return true for leaves, and false for internal nodes. + * + * @param {function} f a leaf function. + * @returns {pv.Nest} this. + */ +pv.Flatten.prototype.leaf = function(f) { + this.keys.length = 0; + this.$leaf = f; + return this; +}; + +/** + * Returns the flattened array. Each entry in the array is an object; each + * object has attributes corresponding to this flatten operator's keys. + * + * @returns an array of elements from the flattened map. + */ +pv.Flatten.prototype.array = function() { + var entries = [], stack = [], keys = this.keys, leaf = this.$leaf; + + /* Recursively visit using the leaf function. */ + if (leaf) { + function recurse(value, i) { + if (leaf(value)) { + entries.push({keys: stack.slice(), value: value}); + } else { + for (var key in value) { + stack.push(key); + recurse(value[key], i + 1); + stack.pop(); + } + } + } + recurse(this.map, 0); + return entries; + } + + /* Recursively visits the specified value. */ + function visit(value, i) { + if (i < keys.length - 1) { + for (var key in value) { + stack.push(key); + visit(value[key], i + 1); + stack.pop(); + } + } else { + entries.push(stack.concat(value)); + } + } + + visit(this.map, 0); + return entries.map(function(stack) { + var m = {}; + for (var i = 0; i < keys.length; i++) { + var k = keys[i], v = stack[i]; + m[k.name] = k.value ? k.value.call(null, v) : v; + } + return m; + }); +}; +/** + * Returns a {@link pv.Vector} for the specified <i>x</i> and <i>y</i> + * coordinate. This is a convenience factory method, equivalent to <tt>new + * pv.Vector(x, y)</tt>. + * + * @see pv.Vector + * @param {number} x the <i>x</i> coordinate. + * @param {number} y the <i>y</i> coordinate. + * @returns {pv.Vector} a vector for the specified coordinates. + */ +pv.vector = function(x, y) { + return new pv.Vector(x, y); +}; + +/** + * Constructs a {@link pv.Vector} for the specified <i>x</i> and <i>y</i> + * coordinate. This constructor should not be invoked directly; use + * {@link pv.vector} instead. + * + * @class Represents a two-dimensional vector; a 2-tuple <i>⟨x, + * y⟩</i>. The intent of this class is to simplify vector math. Note that + * in performance-sensitive cases it may be more efficient to represent 2D + * vectors as simple objects with <tt>x</tt> and <tt>y</tt> attributes, rather + * than using instances of this class. + * + * @param {number} x the <i>x</i> coordinate. + * @param {number} y the <i>y</i> coordinate. + */ +pv.Vector = function(x, y) { + this.x = x; + this.y = y; +}; + +/** + * Returns a vector perpendicular to this vector: <i>⟨-y, x⟩</i>. + * + * @returns {pv.Vector} a perpendicular vector. + */ +pv.Vector.prototype.perp = function() { + return new pv.Vector(-this.y, this.x); +}; + +/** + * Returns a normalized copy of this vector: a vector with the same direction, + * but unit length. If this vector has zero length this method returns a copy of + * this vector. + * + * @returns {pv.Vector} a unit vector. + */ +pv.Vector.prototype.norm = function() { + var l = this.length(); + return this.times(l ? (1 / l) : 1); +}; + +/** + * Returns the magnitude of this vector, defined as <i>sqrt(x * x + y * y)</i>. + * + * @returns {number} a length. + */ +pv.Vector.prototype.length = function() { + return Math.sqrt(this.x * this.x + this.y * this.y); +}; + +/** + * Returns a scaled copy of this vector: <i>⟨x * k, y * k⟩</i>. + * To perform the equivalent divide operation, use <i>1 / k</i>. + * + * @param {number} k the scale factor. + * @returns {pv.Vector} a scaled vector. + */ +pv.Vector.prototype.times = function(k) { + return new pv.Vector(this.x * k, this.y * k); +}; + +/** + * Returns this vector plus the vector <i>v</i>: <i>⟨x + v.x, y + + * v.y⟩</i>. If only one argument is specified, it is interpreted as the + * vector <i>v</i>. + * + * @param {number} x the <i>x</i> coordinate to add. + * @param {number} y the <i>y</i> coordinate to add. + * @returns {pv.Vector} a new vector. + */ +pv.Vector.prototype.plus = function(x, y) { + return (arguments.length == 1) + ? new pv.Vector(this.x + x.x, this.y + x.y) + : new pv.Vector(this.x + x, this.y + y); +}; + +/** + * Returns this vector minus the vector <i>v</i>: <i>⟨x - v.x, y - + * v.y⟩</i>. If only one argument is specified, it is interpreted as the + * vector <i>v</i>. + * + * @param {number} x the <i>x</i> coordinate to subtract. + * @param {number} y the <i>y</i> coordinate to subtract. + * @returns {pv.Vector} a new vector. + */ +pv.Vector.prototype.minus = function(x, y) { + return (arguments.length == 1) + ? new pv.Vector(this.x - x.x, this.y - x.y) + : new pv.Vector(this.x - x, this.y - y); +}; + +/** + * Returns the dot product of this vector and the vector <i>v</i>: <i>x * v.x + + * y * v.y</i>. If only one argument is specified, it is interpreted as the + * vector <i>v</i>. + * + * @param {number} x the <i>x</i> coordinate to dot. + * @param {number} y the <i>y</i> coordinate to dot. + * @returns {number} a dot product. + */ +pv.Vector.prototype.dot = function(x, y) { + return (arguments.length == 1) + ? this.x * x.x + this.y * x.y + : this.x * x + this.y * y; +}; +/** + * Returns a new identity transform. + * + * @class Represents a transformation matrix. The transformation matrix is + * limited to expressing translate and uniform scale transforms only; shearing, + * rotation, general affine, and other transforms are not supported. + * + * <p>The methods on this class treat the transform as immutable, returning a + * copy of the transformation matrix with the specified transform applied. Note, + * alternatively, that the matrix fields can be get and set directly. + */ +pv.Transform = function() {}; +pv.Transform.prototype = {k: 1, x: 0, y: 0}; + +/** + * The scale magnitude; defaults to 1. + * + * @type number + * @name pv.Transform.prototype.k + */ + +/** + * The x-offset; defaults to 0. + * + * @type number + * @name pv.Transform.prototype.x + */ + +/** + * The y-offset; defaults to 0. + * + * @type number + * @name pv.Transform.prototype.y + */ + +/** + * @private The identity transform. + * + * @type pv.Transform + */ +pv.Transform.identity = new pv.Transform(); + +// k 0 x 1 0 a k 0 ka+x +// 0 k y * 0 1 b = 0 k kb+y +// 0 0 1 0 0 1 0 0 1 + +/** + * Returns a translated copy of this transformation matrix. + * + * @param {number} x the x-offset. + * @param {number} y the y-offset. + * @returns {pv.Transform} the translated transformation matrix. + */ +pv.Transform.prototype.translate = function(x, y) { + var v = new pv.Transform(); + v.k = this.k; + v.x = this.k * x + this.x; + v.y = this.k * y + this.y; + return v; +}; + +// k 0 x d 0 0 kd 0 x +// 0 k y * 0 d 0 = 0 kd y +// 0 0 1 0 0 1 0 0 1 + +/** + * Returns a scaled copy of this transformation matrix. + * + * @param {number} k + * @returns {pv.Transform} the scaled transformation matrix. + */ +pv.Transform.prototype.scale = function(k) { + var v = new pv.Transform(); + v.k = this.k * k; + v.x = this.x; + v.y = this.y; + return v; +}; + +/** + * Returns the inverse of this transformation matrix. + * + * @returns {pv.Transform} the inverted transformation matrix. + */ +pv.Transform.prototype.invert = function() { + var v = new pv.Transform(), k = 1 / this.k; + v.k = k; + v.x = -this.x * k; + v.y = -this.y * k; + return v; +}; + +// k 0 x d 0 a kd 0 ka+x +// 0 k y * 0 d b = 0 kd kb+y +// 0 0 1 0 0 1 0 0 1 + +/** + * Returns this matrix post-multiplied by the specified matrix <i>m</i>. + * + * @param {pv.Transform} m + * @returns {pv.Transform} the post-multiplied transformation matrix. + */ +pv.Transform.prototype.times = function(m) { + var v = new pv.Transform(); + v.k = this.k * m.k; + v.x = this.k * m.x + this.x; + v.y = this.k * m.y + this.y; + return v; +}; +/** + * Abstract; see the various scale implementations. + * + * @class Represents a scale; a function that performs a transformation from + * data domain to visual range. For quantitative and quantile scales, the domain + * is expressed as numbers; for ordinal scales, the domain is expressed as + * strings (or equivalently objects with unique string representations). The + * "visual range" may correspond to pixel space, colors, font sizes, and the + * like. + * + * <p>Note that scales are functions, and thus can be used as properties + * directly, assuming that the data associated with a mark is a number. While + * this is convenient for single-use scales, frequently it is desirable to + * define scales globally: + * + * <pre>var y = pv.Scale.linear(0, 100).range(0, 640);</pre> + * + * The <tt>y</tt> scale can now be equivalently referenced within a property: + * + * <pre> .height(function(d) y(d))</pre> + * + * Alternatively, if the data are not simple numbers, the appropriate value can + * be passed to the <tt>y</tt> scale (e.g., <tt>d.foo</tt>). The {@link #by} + * method similarly allows the data to be mapped to a numeric value before + * performing the linear transformation. + * + * @see pv.Scale.quantitative + * @see pv.Scale.quantile + * @see pv.Scale.ordinal + * @extends function + */ +pv.Scale = function() {}; + +/** + * @private Returns a function that interpolators from the start value to the + * end value, given a parameter <i>t</i> in [0, 1]. + * + * @param start the start value. + * @param end the end value. + */ +pv.Scale.interpolator = function(start, end) { + if (typeof start == "number") { + return function(t) { + return t * (end - start) + start; + }; + } + + /* For now, assume color. */ + start = pv.color(start).rgb(); + end = pv.color(end).rgb(); + return function(t) { + var a = start.a * (1 - t) + end.a * t; + if (a < 1e-5) a = 0; // avoid scientific notation + return (start.a == 0) ? pv.rgb(end.r, end.g, end.b, a) + : ((end.a == 0) ? pv.rgb(start.r, start.g, start.b, a) + : pv.rgb( + Math.round(start.r * (1 - t) + end.r * t), + Math.round(start.g * (1 - t) + end.g * t), + Math.round(start.b * (1 - t) + end.b * t), a)); + }; +}; + +/** + * Returns a view of this scale by the specified accessor function <tt>f</tt>. + * Given a scale <tt>y</tt>, <tt>y.by(function(d) d.foo)</tt> is equivalent to + * <tt>function(d) y(d.foo)</tt>. + * + * <p>This method is provided for convenience, such that scales can be + * succinctly defined inline. For example, given an array of data elements that + * have a <tt>score</tt> attribute with the domain [0, 1], the height property + * could be specified as: + * + * <pre> .height(pv.Scale.linear().range(0, 480).by(function(d) d.score))</pre> + * + * This is equivalent to: + * + * <pre> .height(function(d) d.score * 480)</pre> + * + * This method should be used judiciously; it is typically more clear to invoke + * the scale directly, passing in the value to be scaled. + * + * @function + * @name pv.Scale.prototype.by + * @param {function} f an accessor function. + * @returns {pv.Scale} a view of this scale by the specified accessor function. + */ +/** + * Returns a default quantitative, linear, scale for the specified domain. The + * arguments to this constructor are optional, and equivalent to calling + * {@link #domain}. The default domain and range are [0,1]. + * + * <p>This constructor is typically not used directly; see one of the + * quantitative scale implementations instead. + * + * @class Represents an abstract quantitative scale; a function that performs a + * numeric transformation. This class is typically not used directly; see one of + * the quantitative scale implementations (linear, log, root, etc.) + * instead. <style type="text/css">sub{line-height:0}</style> A quantitative + * scale represents a 1-dimensional transformation from a numeric domain of + * input data [<i>d<sub>0</sub></i>, <i>d<sub>1</sub></i>] to a numeric range of + * pixels [<i>r<sub>0</sub></i>, <i>r<sub>1</sub></i>]. In addition to + * readability, scales offer several useful features: + * + * <p>1. The range can be expressed in colors, rather than pixels. For example: + * + * <pre> .fillStyle(pv.Scale.linear(0, 100).range("red", "green"))</pre> + * + * will fill the marks "red" on an input value of 0, "green" on an input value + * of 100, and some color in-between for intermediate values. + * + * <p>2. The domain and range can be subdivided for a non-uniform + * transformation. For example, you may want a diverging color scale that is + * increasingly red for negative values, and increasingly green for positive + * values: + * + * <pre> .fillStyle(pv.Scale.linear(-1, 0, 1).range("red", "white", "green"))</pre> + * + * The domain can be specified as a series of <i>n</i> monotonically-increasing + * values; the range must also be specified as <i>n</i> values, resulting in + * <i>n - 1</i> contiguous linear scales. + * + * <p>3. Quantitative scales can be inverted for interaction. The + * {@link #invert} method takes a value in the output range, and returns the + * corresponding value in the input domain. This is frequently used to convert + * the mouse location (see {@link pv.Mark#mouse}) to a value in the input + * domain. Note that inversion is only supported for numeric ranges, and not + * colors. + * + * <p>4. A scale can be queried for reasonable "tick" values. The {@link #ticks} + * method provides a convenient way to get a series of evenly-spaced rounded + * values in the input domain. Frequently these are used in conjunction with + * {@link pv.Rule} to display tick marks or grid lines. + * + * <p>5. A scale can be "niced" to extend the domain to suitable rounded + * numbers. If the minimum and maximum of the domain are messy because they are + * derived from data, you can use {@link #nice} to round these values down and + * up to even numbers. + * + * @param {number...} domain... optional domain values. + * @see pv.Scale.linear + * @see pv.Scale.log + * @see pv.Scale.root + * @extends pv.Scale + */ +pv.Scale.quantitative = function() { + var d = [0, 1], // default domain + l = [0, 1], // default transformed domain + r = [0, 1], // default range + i = [pv.identity], // default interpolators + type = Number, // default type + n = false, // whether the domain is negative + f = pv.identity, // default forward transform + g = pv.identity, // default inverse transform + tickFormat = String; // default tick formatting function + + /** @private */ + function newDate(x) { + return new Date(x); + } + + /** @private */ + function scale(x) { + var j = pv.search(d, x); + if (j < 0) j = -j - 2; + j = Math.max(0, Math.min(i.length - 1, j)); + return i[j]((f(x) - l[j]) / (l[j + 1] - l[j])); + } + + /** @private */ + scale.transform = function(forward, inverse) { + /** @ignore */ f = function(x) { return n ? -forward(-x) : forward(x); }; + /** @ignore */ g = function(y) { return n ? -inverse(-y) : inverse(y); }; + l = d.map(f); + return this; + }; + + /** + * Sets or gets the input domain. This method can be invoked several ways: + * + * <p>1. <tt>domain(min, ..., max)</tt> + * + * <p>Specifying the domain as a series of numbers is the most explicit and + * recommended approach. Most commonly, two numbers are specified: the minimum + * and maximum value. However, for a diverging scale, or other subdivided + * non-uniform scales, multiple values can be specified. Values can be derived + * from data using {@link pv.min} and {@link pv.max}. For example: + * + * <pre> .domain(0, pv.max(array))</pre> + * + * An alternative method for deriving minimum and maximum values from data + * follows. + * + * <p>2. <tt>domain(array, minf, maxf)</tt> + * + * <p>When both the minimum and maximum value are derived from data, the + * arguments to the <tt>domain</tt> method can be specified as the array of + * data, followed by zero, one or two accessor functions. For example, if the + * array of data is just an array of numbers: + * + * <pre> .domain(array)</pre> + * + * On the other hand, if the array elements are objects representing stock + * values per day, and the domain should consider the stock's daily low and + * daily high: + * + * <pre> .domain(array, function(d) d.low, function(d) d.high)</pre> + * + * The first method of setting the domain is preferred because it is more + * explicit; setting the domain using this second method should be used only + * if brevity is required. + * + * <p>3. <tt>domain()</tt> + * + * <p>Invoking the <tt>domain</tt> method with no arguments returns the + * current domain as an array of numbers. + * + * @function + * @name pv.Scale.quantitative.prototype.domain + * @param {number...} domain... domain values. + * @returns {pv.Scale.quantitative} <tt>this</tt>, or the current domain. + */ + scale.domain = function(array, min, max) { + if (arguments.length) { + var o; // the object we use to infer the domain type + if (array instanceof Array) { + if (arguments.length < 2) min = pv.identity; + if (arguments.length < 3) max = min; + o = array.length && min(array[0]); + d = array.length ? [pv.min(array, min), pv.max(array, max)] : []; + } else { + o = array; + d = Array.prototype.slice.call(arguments).map(Number); + } + if (!d.length) d = [-Infinity, Infinity]; + else if (d.length == 1) d = [d[0], d[0]]; + n = (d[0] || d[d.length - 1]) < 0; + l = d.map(f); + type = (o instanceof Date) ? newDate : Number; + return this; + } + return d.map(type); + }; + + /** + * Sets or gets the output range. This method can be invoked several ways: + * + * <p>1. <tt>range(min, ..., max)</tt> + * + * <p>The range may be specified as a series of numbers or colors. Most + * commonly, two numbers are specified: the minimum and maximum pixel values. + * For a color scale, values may be specified as {@link pv.Color}s or + * equivalent strings. For a diverging scale, or other subdivided non-uniform + * scales, multiple values can be specified. For example: + * + * <pre> .range("red", "white", "green")</pre> + * + * <p>Currently, only numbers and colors are supported as range values. The + * number of range values must exactly match the number of domain values, or + * the behavior of the scale is undefined. + * + * <p>2. <tt>range()</tt> + * + * <p>Invoking the <tt>range</tt> method with no arguments returns the current + * range as an array of numbers or colors. + * + * @function + * @name pv.Scale.quantitative.prototype.range + * @param {...} range... range values. + * @returns {pv.Scale.quantitative} <tt>this</tt>, or the current range. + */ + scale.range = function() { + if (arguments.length) { + r = Array.prototype.slice.call(arguments); + if (!r.length) r = [-Infinity, Infinity]; + else if (r.length == 1) r = [r[0], r[0]]; + i = []; + for (var j = 0; j < r.length - 1; j++) { + i.push(pv.Scale.interpolator(r[j], r[j + 1])); + } + return this; + } + return r; + }; + + /** + * Inverts the specified value in the output range, returning the + * corresponding value in the input domain. This is frequently used to convert + * the mouse location (see {@link pv.Mark#mouse}) to a value in the input + * domain. Inversion is only supported for numeric ranges, and not colors. + * + * <p>Note that this method does not do any rounding or bounds checking. If + * the input domain is discrete (e.g., an array index), the returned value + * should be rounded. If the specified <tt>y</tt> value is outside the range, + * the returned value may be equivalently outside the input domain. + * + * @function + * @name pv.Scale.quantitative.prototype.invert + * @param {number} y a value in the output range (a pixel location). + * @returns {number} a value in the input domain. + */ + scale.invert = function(y) { + var j = pv.search(r, y); + if (j < 0) j = -j - 2; + j = Math.max(0, Math.min(i.length - 1, j)); + return type(g(l[j] + (y - r[j]) / (r[j + 1] - r[j]) * (l[j + 1] - l[j]))); + }; + + /** + * Returns an array of evenly-spaced, suitably-rounded values in the input + * domain. This method attempts to return between 5 and 10 tick values. These + * values are frequently used in conjunction with {@link pv.Rule} to display + * tick marks or grid lines. + * + * @function + * @name pv.Scale.quantitative.prototype.ticks + * @param {number} [m] optional number of desired ticks. + * @returns {number[]} an array input domain values to use as ticks. + */ + scale.ticks = function(m) { + var start = d[0], + end = d[d.length - 1], + reverse = end < start, + min = reverse ? end : start, + max = reverse ? start : end, + span = max - min; + + /* Special case: empty, invalid or infinite span. */ + if (!span || !isFinite(span)) { + if (type == newDate) tickFormat = pv.Format.date("%x"); + return [type(min)]; + } + + /* Special case: dates. */ + if (type == newDate) { + /* Floor the date d given the precision p. */ + function floor(d, p) { + switch (p) { + case 31536e6: d.setMonth(0); + case 2592e6: d.setDate(1); + case 6048e5: if (p == 6048e5) d.setDate(d.getDate() - d.getDay()); + case 864e5: d.setHours(0); + case 36e5: d.setMinutes(0); + case 6e4: d.setSeconds(0); + case 1e3: d.setMilliseconds(0); + } + } + + var precision, format, increment, step = 1; + if (span >= 3 * 31536e6) { + precision = 31536e6; + format = "%Y"; + /** @ignore */ increment = function(d) { d.setFullYear(d.getFullYear() + step); }; + } else if (span >= 3 * 2592e6) { + precision = 2592e6; + format = "%m/%Y"; + /** @ignore */ increment = function(d) { d.setMonth(d.getMonth() + step); }; + } else if (span >= 3 * 6048e5) { + precision = 6048e5; + format = "%m/%d"; + /** @ignore */ increment = function(d) { d.setDate(d.getDate() + 7 * step); }; + } else if (span >= 3 * 864e5) { + precision = 864e5; + format = "%m/%d"; + /** @ignore */ increment = function(d) { d.setDate(d.getDate() + step); }; + } else if (span >= 3 * 36e5) { + precision = 36e5; + format = "%I:%M %p"; + /** @ignore */ increment = function(d) { d.setHours(d.getHours() + step); }; + } else if (span >= 3 * 6e4) { + precision = 6e4; + format = "%I:%M %p"; + /** @ignore */ increment = function(d) { d.setMinutes(d.getMinutes() + step); }; + } else if (span >= 3 * 1e3) { + precision = 1e3; + format = "%I:%M:%S"; + /** @ignore */ increment = function(d) { d.setSeconds(d.getSeconds() + step); }; + } else { + precision = 1; + format = "%S.%Qs"; + /** @ignore */ increment = function(d) { d.setTime(d.getTime() + step); }; + } + tickFormat = pv.Format.date(format); + + var date = new Date(min), dates = []; + floor(date, precision); + + /* If we'd generate too many ticks, skip some!. */ + var n = span / precision; + if (n > 10) { + switch (precision) { + case 36e5: { + step = (n > 20) ? 6 : 3; + date.setHours(Math.floor(date.getHours() / step) * step); + break; + } + case 2592e6: { + step = 3; // seasons + date.setMonth(Math.floor(date.getMonth() / step) * step); + break; + } + case 6e4: { + step = (n > 30) ? 15 : ((n > 15) ? 10 : 5); + date.setMinutes(Math.floor(date.getMinutes() / step) * step); + break; + } + case 1e3: { + step = (n > 90) ? 15 : ((n > 60) ? 10 : 5); + date.setSeconds(Math.floor(date.getSeconds() / step) * step); + break; + } + case 1: { + step = (n > 1000) ? 250 : ((n > 200) ? 100 : ((n > 100) ? 50 : ((n > 50) ? 25 : 5))); + date.setMilliseconds(Math.floor(date.getMilliseconds() / step) * step); + break; + } + default: { + step = pv.logCeil(n / 15, 10); + if (n / step < 2) step /= 5; + else if (n / step < 5) step /= 2; + date.setFullYear(Math.floor(date.getFullYear() / step) * step); + break; + } + } + } + + while (true) { + increment(date); + if (date > max) break; + dates.push(new Date(date)); + } + return reverse ? dates.reverse() : dates; + } + + /* Normal case: numbers. */ + if (!arguments.length) m = 10; + var step = pv.logFloor(span / m, 10), + err = m / (span / step); + if (err <= .15) step *= 10; + else if (err <= .35) step *= 5; + else if (err <= .75) step *= 2; + var start = Math.ceil(min / step) * step, + end = Math.floor(max / step) * step; + tickFormat = pv.Format.number() + .fractionDigits(Math.max(0, -Math.floor(pv.log(step, 10) + .01))); + var ticks = pv.range(start, end + step, step); + return reverse ? ticks.reverse() : ticks; + }; + + /** + * Formats the specified tick value using the appropriate precision, based on + * the step interval between tick marks. If {@link #ticks} has not been called, + * the argument is converted to a string, but no formatting is applied. + * + * @function + * @name pv.Scale.quantitative.prototype.tickFormat + * @param {number} t a tick value. + * @returns {string} a formatted tick value. + */ + scale.tickFormat = function (t) { return tickFormat(t); }; + + /** + * "Nices" this scale, extending the bounds of the input domain to + * evenly-rounded values. Nicing is useful if the domain is computed + * dynamically from data, and may be irregular. For example, given a domain of + * [0.20147987687960267, 0.996679553296417], a call to <tt>nice()</tt> might + * extend the domain to [0.2, 1]. + * + * <p>This method must be invoked each time after setting the domain. + * + * @function + * @name pv.Scale.quantitative.prototype.nice + * @returns {pv.Scale.quantitative} <tt>this</tt>. + */ + scale.nice = function() { + if (d.length != 2) return this; // TODO support non-uniform domains + var start = d[0], + end = d[d.length - 1], + reverse = end < start, + min = reverse ? end : start, + max = reverse ? start : end, + span = max - min; + + /* Special case: empty, invalid or infinite span. */ + if (!span || !isFinite(span)) return this; + + var step = Math.pow(10, Math.round(Math.log(span) / Math.log(10)) - 1); + d = [Math.floor(min / step) * step, Math.ceil(max / step) * step]; + if (reverse) d.reverse(); + l = d.map(f); + return this; + }; + + /** + * Returns a view of this scale by the specified accessor function <tt>f</tt>. + * Given a scale <tt>y</tt>, <tt>y.by(function(d) d.foo)</tt> is equivalent to + * <tt>function(d) y(d.foo)</tt>. + * + * <p>This method is provided for convenience, such that scales can be + * succinctly defined inline. For example, given an array of data elements + * that have a <tt>score</tt> attribute with the domain [0, 1], the height + * property could be specified as: + * + * <pre> .height(pv.Scale.linear().range(0, 480).by(function(d) d.score))</pre> + * + * This is equivalent to: + * + * <pre> .height(function(d) d.score * 480)</pre> + * + * This method should be used judiciously; it is typically more clear to + * invoke the scale directly, passing in the value to be scaled. + * + * @function + * @name pv.Scale.quantitative.prototype.by + * @param {function} f an accessor function. + * @returns {pv.Scale.quantitative} a view of this scale by the specified + * accessor function. + */ + scale.by = function(f) { + function by() { return scale(f.apply(this, arguments)); } + for (var method in scale) by[method] = scale[method]; + return by; + }; + + scale.domain.apply(scale, arguments); + return scale; +}; +/** + * Returns a linear scale for the specified domain. The arguments to this + * constructor are optional, and equivalent to calling {@link #domain}. + * The default domain and range are [0,1]. + * + * @class Represents a linear scale; a function that performs a linear + * transformation. <style type="text/css">sub{line-height:0}</style> Most + * commonly, a linear scale represents a 1-dimensional linear transformation + * from a numeric domain of input data [<i>d<sub>0</sub></i>, + * <i>d<sub>1</sub></i>] to a numeric range of pixels [<i>r<sub>0</sub></i>, + * <i>r<sub>1</sub></i>]. The equation for such a scale is: + * + * <blockquote><i>f(x) = (x - d<sub>0</sub>) / (d<sub>1</sub> - d<sub>0</sub>) * + * (r<sub>1</sub> - r<sub>0</sub>) + r<sub>0</sub></i></blockquote> + * + * For example, a linear scale from the domain [0, 100] to range [0, 640]: + * + * <blockquote><i>f(x) = (x - 0) / (100 - 0) * (640 - 0) + 0</i><br> + * <i>f(x) = x / 100 * 640</i><br> + * <i>f(x) = x * 6.4</i><br> + * </blockquote> + * + * Thus, saying + * + * <pre> .height(function(d) d * 6.4)</pre> + * + * is identical to + * + * <pre> .height(pv.Scale.linear(0, 100).range(0, 640))</pre> + * + * Note that the scale is itself a function, and thus can be used as a property + * directly, assuming that the data associated with a mark is a number. While + * this is convenient for single-use scales, frequently it is desirable to + * define scales globally: + * + * <pre>var y = pv.Scale.linear(0, 100).range(0, 640);</pre> + * + * The <tt>y</tt> scale can now be equivalently referenced within a property: + * + * <pre> .height(function(d) y(d))</pre> + * + * Alternatively, if the data are not simple numbers, the appropriate value can + * be passed to the <tt>y</tt> scale (e.g., <tt>d.foo</tt>). The {@link #by} + * method similarly allows the data to be mapped to a numeric value before + * performing the linear transformation. + * + * @param {number...} domain... optional domain values. + * @extends pv.Scale.quantitative + */ +pv.Scale.linear = function() { + var scale = pv.Scale.quantitative(); + scale.domain.apply(scale, arguments); + return scale; +}; +/** + * Returns a log scale for the specified domain. The arguments to this + * constructor are optional, and equivalent to calling {@link #domain}. + * The default domain is [1,10] and the default range is [0,1]. + * + * @class Represents a log scale. <style + * type="text/css">sub{line-height:0}</style> Most commonly, a log scale + * represents a 1-dimensional log transformation from a numeric domain of input + * data [<i>d<sub>0</sub></i>, <i>d<sub>1</sub></i>] to a numeric range of + * pixels [<i>r<sub>0</sub></i>, <i>r<sub>1</sub></i>]. The equation for such a + * scale is: + * + * <blockquote><i>f(x) = (log(x) - log(d<sub>0</sub>)) / (log(d<sub>1</sub>) - + * log(d<sub>0</sub>)) * (r<sub>1</sub> - r<sub>0</sub>) + + * r<sub>0</sub></i></blockquote> + * + * where <i>log(x)</i> represents the zero-symmetric logarthim of <i>x</i> using + * the scale's associated base (default: 10, see {@link pv.logSymmetric}). For + * example, a log scale from the domain [1, 100] to range [0, 640]: + * + * <blockquote><i>f(x) = (log(x) - log(1)) / (log(100) - log(1)) * (640 - 0) + 0</i><br> + * <i>f(x) = log(x) / 2 * 640</i><br> + * <i>f(x) = log(x) * 320</i><br> + * </blockquote> + * + * Thus, saying + * + * <pre> .height(function(d) Math.log(d) * 138.974)</pre> + * + * is equivalent to + * + * <pre> .height(pv.Scale.log(1, 100).range(0, 640))</pre> + * + * Note that the scale is itself a function, and thus can be used as a property + * directly, assuming that the data associated with a mark is a number. While + * this is convenient for single-use scales, frequently it is desirable to + * define scales globally: + * + * <pre>var y = pv.Scale.log(1, 100).range(0, 640);</pre> + * + * The <tt>y</tt> scale can now be equivalently referenced within a property: + * + * <pre> .height(function(d) y(d))</pre> + * + * Alternatively, if the data are not simple numbers, the appropriate value can + * be passed to the <tt>y</tt> scale (e.g., <tt>d.foo</tt>). The {@link #by} + * method similarly allows the data to be mapped to a numeric value before + * performing the log transformation. + * + * @param {number...} domain... optional domain values. + * @extends pv.Scale.quantitative + */ +pv.Scale.log = function() { + var scale = pv.Scale.quantitative(1, 10), + b, // logarithm base + p, // cached Math.log(b) + /** @ignore */ log = function(x) { return Math.log(x) / p; }, + /** @ignore */ pow = function(y) { return Math.pow(b, y); }; + + /** + * Returns an array of evenly-spaced, suitably-rounded values in the input + * domain. These values are frequently used in conjunction with + * {@link pv.Rule} to display tick marks or grid lines. + * + * @function + * @name pv.Scale.log.prototype.ticks + * @returns {number[]} an array input domain values to use as ticks. + */ + scale.ticks = function() { + // TODO support non-uniform domains + var d = scale.domain(), + n = d[0] < 0, + i = Math.floor(n ? -log(-d[0]) : log(d[0])), + j = Math.ceil(n ? -log(-d[1]) : log(d[1])), + ticks = []; + if (n) { + ticks.push(-pow(-i)); + for (; i++ < j;) for (var k = b - 1; k > 0; k--) ticks.push(-pow(-i) * k); + } else { + for (; i < j; i++) for (var k = 1; k < b; k++) ticks.push(pow(i) * k); + ticks.push(pow(i)); + } + for (i = 0; ticks[i] < d[0]; i++); // strip small values + for (j = ticks.length; ticks[j - 1] > d[1]; j--); // strip big values + return ticks.slice(i, j); + }; + + /** + * Formats the specified tick value using the appropriate precision, assuming + * base 10. + * + * @function + * @name pv.Scale.log.prototype.tickFormat + * @param {number} t a tick value. + * @returns {string} a formatted tick value. + */ + scale.tickFormat = function(t) { + return t.toPrecision(1); + }; + + /** + * "Nices" this scale, extending the bounds of the input domain to + * evenly-rounded values. This method uses {@link pv.logFloor} and + * {@link pv.logCeil}. Nicing is useful if the domain is computed dynamically + * from data, and may be irregular. For example, given a domain of + * [0.20147987687960267, 0.996679553296417], a call to <tt>nice()</tt> might + * extend the domain to [0.1, 1]. + * + * <p>This method must be invoked each time after setting the domain (and + * base). + * + * @function + * @name pv.Scale.log.prototype.nice + * @returns {pv.Scale.log} <tt>this</tt>. + */ + scale.nice = function() { + // TODO support non-uniform domains + var d = scale.domain(); + return scale.domain(pv.logFloor(d[0], b), pv.logCeil(d[1], b)); + }; + + /** + * Sets or gets the logarithm base. Defaults to 10. + * + * @function + * @name pv.Scale.log.prototype.base + * @param {number} [v] the new base. + * @returns {pv.Scale.log} <tt>this</tt>, or the current base. + */ + scale.base = function(v) { + if (arguments.length) { + b = Number(v); + p = Math.log(b); + scale.transform(log, pow); // update transformed domain + return this; + } + return b; + }; + + scale.domain.apply(scale, arguments); + return scale.base(10); +}; +/** + * Returns a root scale for the specified domain. The arguments to this + * constructor are optional, and equivalent to calling {@link #domain}. + * The default domain and range are [0,1]. + * + * @class Represents a root scale; a function that performs a power + * transformation. <style type="text/css">sub{line-height:0}</style> Most + * commonly, a root scale represents a 1-dimensional root transformation from a + * numeric domain of input data [<i>d<sub>0</sub></i>, <i>d<sub>1</sub></i>] to + * a numeric range of pixels [<i>r<sub>0</sub></i>, <i>r<sub>1</sub></i>]. + * + * <p>Note that the scale is itself a function, and thus can be used as a + * property directly, assuming that the data associated with a mark is a + * number. While this is convenient for single-use scales, frequently it is + * desirable to define scales globally: + * + * <pre>var y = pv.Scale.root(0, 100).range(0, 640);</pre> + * + * The <tt>y</tt> scale can now be equivalently referenced within a property: + * + * <pre> .height(function(d) y(d))</pre> + * + * Alternatively, if the data are not simple numbers, the appropriate value can + * be passed to the <tt>y</tt> scale (e.g., <tt>d.foo</tt>). The {@link #by} + * method similarly allows the data to be mapped to a numeric value before + * performing the root transformation. + * + * @param {number...} domain... optional domain values. + * @extends pv.Scale.quantitative + */ +pv.Scale.root = function() { + var scale = pv.Scale.quantitative(); + + /** + * Sets or gets the exponent; defaults to 2. + * + * @function + * @name pv.Scale.root.prototype.power + * @param {number} [v] the new exponent. + * @returns {pv.Scale.root} <tt>this</tt>, or the current base. + */ + scale.power = function(v) { + if (arguments.length) { + var b = Number(v), p = 1 / b; + scale.transform( + function(x) { return Math.pow(x, p); }, + function(y) { return Math.pow(y, b); }); + return this; + } + return b; + }; + + scale.domain.apply(scale, arguments); + return scale.power(2); +}; +/** + * Returns an ordinal scale for the specified domain. The arguments to this + * constructor are optional, and equivalent to calling {@link #domain}. + * + * @class Represents an ordinal scale. <style + * type="text/css">sub{line-height:0}</style> An ordinal scale represents a + * pairwise mapping from <i>n</i> discrete values in the input domain to + * <i>n</i> discrete values in the output range. For example, an ordinal scale + * might map a domain of species ["setosa", "versicolor", "virginica"] to colors + * ["red", "green", "blue"]. Thus, saying + * + * <pre> .fillStyle(function(d) { + * switch (d.species) { + * case "setosa": return "red"; + * case "versicolor": return "green"; + * case "virginica": return "blue"; + * } + * })</pre> + * + * is equivalent to + * + * <pre> .fillStyle(pv.Scale.ordinal("setosa", "versicolor", "virginica") + * .range("red", "green", "blue") + * .by(function(d) d.species))</pre> + * + * If the mapping from species to color does not need to be specified + * explicitly, the domain can be omitted. In this case it will be inferred + * lazily from the data: + * + * <pre> .fillStyle(pv.colors("red", "green", "blue") + * .by(function(d) d.species))</pre> + * + * When the domain is inferred, the first time the scale is invoked, the first + * element from the range will be returned. Subsequent calls with unique values + * will return subsequent elements from the range. If the inferred domain grows + * larger than the range, range values will be reused. However, it is strongly + * recommended that the domain and the range contain the same number of + * elements. + * + * <p>A range can be discretized from a continuous interval (e.g., for pixel + * positioning) by using {@link #split}, {@link #splitFlush} or + * {@link #splitBanded} after the domain has been set. For example, if + * <tt>states</tt> is an array of the fifty U.S. state names, the state name can + * be encoded in the left position: + * + * <pre> .left(pv.Scale.ordinal(states) + * .split(0, 640) + * .by(function(d) d.state))</pre> + * + * <p>N.B.: ordinal scales are not invertible (at least not yet), since the + * domain and range and discontinuous. A workaround is to use a linear scale. + * + * @param {...} domain... optional domain values. + * @extends pv.Scale + * @see pv.colors + */ +pv.Scale.ordinal = function() { + var d = [], i = {}, r = [], band = 0; + + /** @private */ + function scale(x) { + if (!(x in i)) i[x] = d.push(x) - 1; + return r[i[x] % r.length]; + } + + /** + * Sets or gets the input domain. This method can be invoked several ways: + * + * <p>1. <tt>domain(values...)</tt> + * + * <p>Specifying the domain as a series of values is the most explicit and + * recommended approach. However, if the domain values are derived from data, + * you may find the second method more appropriate. + * + * <p>2. <tt>domain(array, f)</tt> + * + * <p>Rather than enumerating the domain values as explicit arguments to this + * method, you can specify a single argument of an array. In addition, you can + * specify an optional accessor function to extract the domain values from the + * array. + * + * <p>3. <tt>domain()</tt> + * + * <p>Invoking the <tt>domain</tt> method with no arguments returns the + * current domain as an array. + * + * @function + * @name pv.Scale.ordinal.prototype.domain + * @param {...} domain... domain values. + * @returns {pv.Scale.ordinal} <tt>this</tt>, or the current domain. + */ + scale.domain = function(array, f) { + if (arguments.length) { + array = (array instanceof Array) + ? ((arguments.length > 1) ? pv.map(array, f) : array) + : Array.prototype.slice.call(arguments); + + /* Filter the specified ordinals to their unique values. */ + d = []; + var seen = {}; + for (var j = 0; j < array.length; j++) { + var o = array[j]; + if (!(o in seen)) { + seen[o] = true; + d.push(o); + } + } + + i = pv.numerate(d); + return this; + } + return d; + }; + + /** + * Sets or gets the output range. This method can be invoked several ways: + * + * <p>1. <tt>range(values...)</tt> + * + * <p>Specifying the range as a series of values is the most explicit and + * recommended approach. However, if the range values are derived from data, + * you may find the second method more appropriate. + * + * <p>2. <tt>range(array, f)</tt> + * + * <p>Rather than enumerating the range values as explicit arguments to this + * method, you can specify a single argument of an array. In addition, you can + * specify an optional accessor function to extract the range values from the + * array. + * + * <p>3. <tt>range()</tt> + * + * <p>Invoking the <tt>range</tt> method with no arguments returns the + * current range as an array. + * + * @function + * @name pv.Scale.ordinal.prototype.range + * @param {...} range... range values. + * @returns {pv.Scale.ordinal} <tt>this</tt>, or the current range. + */ + scale.range = function(array, f) { + if (arguments.length) { + r = (array instanceof Array) + ? ((arguments.length > 1) ? pv.map(array, f) : array) + : Array.prototype.slice.call(arguments); + if (typeof r[0] == "string") r = r.map(pv.color); + return this; + } + return r; + }; + + /** + * Sets the range from the given continuous interval. The interval + * [<i>min</i>, <i>max</i>] is subdivided into <i>n</i> equispaced points, + * where <i>n</i> is the number of (unique) values in the domain. The first + * and last point are offset from the edge of the range by half the distance + * between points. + * + * <p>This method must be called <i>after</i> the domain is set. + * + * @function + * @name pv.Scale.ordinal.prototype.split + * @param {number} min minimum value of the output range. + * @param {number} max maximum value of the output range. + * @returns {pv.Scale.ordinal} <tt>this</tt>. + * @see #splitFlush + * @see #splitBanded + */ + scale.split = function(min, max) { + var step = (max - min) / this.domain().length; + r = pv.range(min + step / 2, max, step); + return this; + }; + + /** + * Sets the range from the given continuous interval. The interval + * [<i>min</i>, <i>max</i>] is subdivided into <i>n</i> equispaced points, + * where <i>n</i> is the number of (unique) values in the domain. The first + * and last point are exactly on the edge of the range. + * + * <p>This method must be called <i>after</i> the domain is set. + * + * @function + * @name pv.Scale.ordinal.prototype.splitFlush + * @param {number} min minimum value of the output range. + * @param {number} max maximum value of the output range. + * @returns {pv.Scale.ordinal} <tt>this</tt>. + * @see #split + */ + scale.splitFlush = function(min, max) { + var n = this.domain().length, step = (max - min) / (n - 1); + r = (n == 1) ? [(min + max) / 2] + : pv.range(min, max + step / 2, step); + return this; + }; + + /** + * Sets the range from the given continuous interval. The interval + * [<i>min</i>, <i>max</i>] is subdivided into <i>n</i> equispaced bands, + * where <i>n</i> is the number of (unique) values in the domain. The first + * and last band are offset from the edge of the range by the distance between + * bands. + * + * <p>The band width argument, <tt>band</tt>, is typically in the range [0, 1] + * and defaults to 1. This fraction corresponds to the amount of space in the + * range to allocate to the bands, as opposed to padding. A value of 0.5 means + * that the band width will be equal to the padding width. The computed + * absolute band width can be retrieved from the range as + * <tt>scale.range().band</tt>. + * + * <p>If the band width argument is negative, this method will allocate bands + * of a <i>fixed</i> width <tt>-band</tt>, rather than a relative fraction of + * the available space. + * + * <p>Tip: to inset the bands by a fixed amount <tt>p</tt>, specify a minimum + * value of <tt>min + p</tt> (or simply <tt>p</tt>, if <tt>min</tt> is + * 0). Then set the mark width to <tt>scale.range().band - p</tt>. + * + * <p>This method must be called <i>after</i> the domain is set. + * + * @function + * @name pv.Scale.ordinal.prototype.splitBanded + * @param {number} min minimum value of the output range. + * @param {number} max maximum value of the output range. + * @param {number} [band] the fractional band width in [0, 1]; defaults to 1. + * @returns {pv.Scale.ordinal} <tt>this</tt>. + * @see #split + */ + scale.splitBanded = function(min, max, band) { + if (arguments.length < 3) band = 1; + if (band < 0) { + var n = this.domain().length, + total = -band * n, + remaining = max - min - total, + padding = remaining / (n + 1); + r = pv.range(min + padding, max, padding - band); + r.band = -band; + } else { + var step = (max - min) / (this.domain().length + (1 - band)); + r = pv.range(min + step * (1 - band), max, step); + r.band = step * band; + } + return this; + }; + + /** + * Returns a view of this scale by the specified accessor function <tt>f</tt>. + * Given a scale <tt>y</tt>, <tt>y.by(function(d) d.foo)</tt> is equivalent to + * <tt>function(d) y(d.foo)</tt>. This method should be used judiciously; it + * is typically more clear to invoke the scale directly, passing in the value + * to be scaled. + * + * @function + * @name pv.Scale.ordinal.prototype.by + * @param {function} f an accessor function. + * @returns {pv.Scale.ordinal} a view of this scale by the specified accessor + * function. + */ + scale.by = function(f) { + function by() { return scale(f.apply(this, arguments)); } + for (var method in scale) by[method] = scale[method]; + return by; + }; + + scale.domain.apply(scale, arguments); + return scale; +}; +/** + * Constructs a default quantile scale. The arguments to this constructor are + * optional, and equivalent to calling {@link #domain}. The default domain is + * the empty set, and the default range is [0,1]. + * + * @class Represents a quantile scale; a function that maps from a value within + * a sortable domain to a quantized numeric range. Typically, the domain is a + * set of numbers, but any sortable value (such as strings) can be used as the + * domain of a quantile scale. The range defaults to [0,1], with 0 corresponding + * to the smallest value in the domain, 1 the largest, .5 the median, etc. + * + * <p>By default, the number of quantiles in the range corresponds to the number + * of values in the domain. The {@link #quantiles} method can be used to specify + * an explicit number of quantiles; for example, <tt>quantiles(4)</tt> produces + * a standard quartile scale. A quartile scale's range is a set of four discrete + * values, such as [0, 1/3, 2/3, 1]. Calling the {@link #range} method will + * scale these discrete values accordingly, similar to {@link + * pv.Scale.ordinal#splitFlush}. + * + * <p>For example, given the strings ["c", "a", "b"], a default quantile scale: + * + * <pre>pv.Scale.quantile("c", "a", "b")</pre> + * + * will return 0 for "a", .5 for "b", and 1 for "c". + * + * @extends pv.Scale + */ +pv.Scale.quantile = function() { + var n = -1, // number of quantiles + j = -1, // max quantile index + q = [], // quantile boundaries + d = [], // domain + y = pv.Scale.linear(); // range + + /** @private */ + function scale(x) { + return y(Math.max(0, Math.min(j, pv.search.index(q, x) - 1)) / j); + } + + /** + * Sets or gets the quantile boundaries. By default, each element in the + * domain is in its own quantile. If the argument to this method is a number, + * it specifies the number of equal-sized quantiles by which to divide the + * domain. + * + * <p>If no arguments are specified, this method returns the quantile + * boundaries; the first element is always the minimum value of the domain, + * and the last element is the maximum value of the domain. Thus, the length + * of the returned array is always one greater than the number of quantiles. + * + * @function + * @name pv.Scale.quantile.prototype.quantiles + * @param {number} x the number of quantiles. + */ + scale.quantiles = function(x) { + if (arguments.length) { + n = Number(x); + if (n < 0) { + q = [d[0]].concat(d); + j = d.length - 1; + } else { + q = []; + q[0] = d[0]; + for (var i = 1; i <= n; i++) { + q[i] = d[~~(i * (d.length - 1) / n)]; + } + j = n - 1; + } + return this; + } + return q; + }; + + /** + * Sets or gets the input domain. This method can be invoked several ways: + * + * <p>1. <tt>domain(values...)</tt> + * + * <p>Specifying the domain as a series of values is the most explicit and + * recommended approach. However, if the domain values are derived from data, + * you may find the second method more appropriate. + * + * <p>2. <tt>domain(array, f)</tt> + * + * <p>Rather than enumerating the domain values as explicit arguments to this + * method, you can specify a single argument of an array. In addition, you can + * specify an optional accessor function to extract the domain values from the + * array. + * + * <p>3. <tt>domain()</tt> + * + * <p>Invoking the <tt>domain</tt> method with no arguments returns the + * current domain as an array. + * + * @function + * @name pv.Scale.quantile.prototype.domain + * @param {...} domain... domain values. + * @returns {pv.Scale.quantile} <tt>this</tt>, or the current domain. + */ + scale.domain = function(array, f) { + if (arguments.length) { + d = (array instanceof Array) + ? pv.map(array, f) + : Array.prototype.slice.call(arguments); + d.sort(pv.naturalOrder); + scale.quantiles(n); // recompute quantiles + return this; + } + return d; + }; + + /** + * Sets or gets the output range. This method can be invoked several ways: + * + * <p>1. <tt>range(min, ..., max)</tt> + * + * <p>The range may be specified as a series of numbers or colors. Most + * commonly, two numbers are specified: the minimum and maximum pixel values. + * For a color scale, values may be specified as {@link pv.Color}s or + * equivalent strings. For a diverging scale, or other subdivided non-uniform + * scales, multiple values can be specified. For example: + * + * <pre> .range("red", "white", "green")</pre> + * + * <p>Currently, only numbers and colors are supported as range values. The + * number of range values must exactly match the number of domain values, or + * the behavior of the scale is undefined. + * + * <p>2. <tt>range()</tt> + * + * <p>Invoking the <tt>range</tt> method with no arguments returns the current + * range as an array of numbers or colors. + * + * @function + * @name pv.Scale.quantile.prototype.range + * @param {...} range... range values. + * @returns {pv.Scale.quantile} <tt>this</tt>, or the current range. + */ + scale.range = function() { + if (arguments.length) { + y.range.apply(y, arguments); + return this; + } + return y.range(); + }; + + /** + * Returns a view of this scale by the specified accessor function <tt>f</tt>. + * Given a scale <tt>y</tt>, <tt>y.by(function(d) d.foo)</tt> is equivalent to + * <tt>function(d) y(d.foo)</tt>. + * + * <p>This method is provided for convenience, such that scales can be + * succinctly defined inline. For example, given an array of data elements + * that have a <tt>score</tt> attribute with the domain [0, 1], the height + * property could be specified as: + * + * <pre>.height(pv.Scale.linear().range(0, 480).by(function(d) d.score))</pre> + * + * This is equivalent to: + * + * <pre>.height(function(d) d.score * 480)</pre> + * + * This method should be used judiciously; it is typically more clear to + * invoke the scale directly, passing in the value to be scaled. + * + * @function + * @name pv.Scale.quantile.prototype.by + * @param {function} f an accessor function. + * @returns {pv.Scale.quantile} a view of this scale by the specified + * accessor function. + */ + scale.by = function(f) { + function by() { return scale(f.apply(this, arguments)); } + for (var method in scale) by[method] = scale[method]; + return by; + }; + + scale.domain.apply(scale, arguments); + return scale; +}; +/** + * Returns a histogram operator for the specified data, with an optional + * accessor function. If the data specified is not an array of numbers, an + * accessor function must be specified to map the data to numeric values. + * + * @class Represents a histogram operator. + * + * @param {array} data an array of numbers or objects. + * @param {function} [f] an optional accessor function. + */ +pv.histogram = function(data, f) { + var frequency = true; + return { + + /** + * Returns the computed histogram bins. An optional array of numbers, + * <tt>ticks</tt>, may be specified as the break points. If the ticks are + * not specified, default ticks will be computed using a linear scale on the + * data domain. + * + * <p>The returned array contains {@link pv.histogram.Bin}s. The <tt>x</tt> + * attribute corresponds to the bin's start value (inclusive), while the + * <tt>dx</tt> attribute stores the bin size (end - start). The <tt>y</tt> + * attribute stores either the frequency count or probability, depending on + * how the histogram operator has been configured. + * + * <p>The {@link pv.histogram.Bin} objects are themselves arrays, containing + * the data elements present in each bin, i.e., the elements in the + * <tt>data</tt> array (prior to invoking the accessor function, if any). + * For example, if the data represented countries, and the accessor function + * returned the GDP of each country, the returned bins would be arrays of + * countries (not GDPs). + * + * @function + * @name pv.histogram.prototype.bins + * @param {array} [ticks] + * @returns {array} + */ /** @private */ + bins: function(ticks) { + var x = pv.map(data, f), bins = []; + + /* Initialize default ticks. */ + if (!arguments.length) ticks = pv.Scale.linear(x).ticks(); + + /* Initialize the bins. */ + for (var i = 0; i < ticks.length - 1; i++) { + var bin = bins[i] = []; + bin.x = ticks[i]; + bin.dx = ticks[i + 1] - ticks[i]; + bin.y = 0; + } + + /* Count the number of samples per bin. */ + for (var i = 0; i < x.length; i++) { + var j = pv.search.index(ticks, x[i]) - 1, + bin = bins[Math.max(0, Math.min(bins.length - 1, j))]; + bin.y++; + bin.push(data[i]); + } + + /* Convert frequencies to probabilities. */ + if (!frequency) for (var i = 0; i < bins.length; i++) { + bins[i].y /= x.length; + } + + return bins; + }, + + /** + * Sets or gets whether this histogram operator returns frequencies or + * probabilities. + * + * @function + * @name pv.histogram.prototype.frequency + * @param {boolean} [x] + * @returns {pv.histogram} this. + */ /** @private */ + frequency: function(x) { + if (arguments.length) { + frequency = Boolean(x); + return this; + } + return frequency; + } + }; +}; + +/** + * @class Represents a bin returned by the {@link pv.histogram} operator. Bins + * are themselves arrays containing the data elements present in the given bin + * (prior to the accessor function being invoked to convert the data object to a + * numeric value). These bin arrays have additional attributes with meta + * information about the bin. + * + * @name pv.histogram.Bin + * @extends array + * @see pv.histogram + */ + +/** + * The start value of the bin's range. + * + * @type number + * @name pv.histogram.Bin.prototype.x + */ + +/** + * The magnitude value of the bin's range; end - start. + * + * @type number + * @name pv.histogram.Bin.prototype.dx + */ + +/** + * The frequency or probability of the bin, depending on how the histogram + * operator was configured. + * + * @type number + * @name pv.histogram.Bin.prototype.y + */ +/** + * Returns the {@link pv.Color} for the specified color format string. Colors + * may have an associated opacity, or alpha channel. Color formats are specified + * by CSS Color Modular Level 3, using either in RGB or HSL color space. For + * example:<ul> + * + * <li>#f00 // #rgb + * <li>#ff0000 // #rrggbb + * <li>rgb(255, 0, 0) + * <li>rgb(100%, 0%, 0%) + * <li>hsl(0, 100%, 50%) + * <li>rgba(0, 0, 255, 0.5) + * <li>hsla(120, 100%, 50%, 1) + * + * </ul>The SVG 1.0 color keywords names are also supported, such as "aliceblue" + * and "yellowgreen". The "transparent" keyword is supported for fully- + * transparent black. + * + * <p>If the <tt>format</tt> argument is already an instance of <tt>Color</tt>, + * the argument is returned with no further processing. + * + * @param {string} format the color specification string, such as "#f00". + * @returns {pv.Color} the corresponding <tt>Color</tt>. + * @see <a href="http://www.w3.org/TR/SVG/types.html#ColorKeywords">SVG color + * keywords</a> + * @see <a href="http://www.w3.org/TR/css3-color/">CSS3 color module</a> + */ +pv.color = function(format) { + if (format.rgb) return format.rgb(); + + /* Handle hsl, rgb. */ + var m1 = /([a-z]+)\((.*)\)/i.exec(format); + if (m1) { + var m2 = m1[2].split(","), a = 1; + switch (m1[1]) { + case "hsla": + case "rgba": { + a = parseFloat(m2[3]); + if (!a) return pv.Color.transparent; + break; + } + } + switch (m1[1]) { + case "hsla": + case "hsl": { + var h = parseFloat(m2[0]), // degrees + s = parseFloat(m2[1]) / 100, // percentage + l = parseFloat(m2[2]) / 100; // percentage + return (new pv.Color.Hsl(h, s, l, a)).rgb(); + } + case "rgba": + case "rgb": { + function parse(c) { // either integer or percentage + var f = parseFloat(c); + return (c[c.length - 1] == '%') ? Math.round(f * 2.55) : f; + } + var r = parse(m2[0]), g = parse(m2[1]), b = parse(m2[2]); + return pv.rgb(r, g, b, a); + } + } + } + + /* Named colors. */ + var named = pv.Color.names[format]; + if (named) return named; + + /* Hexadecimal colors: #rgb and #rrggbb. */ + if (format.charAt(0) == "#") { + var r, g, b; + if (format.length == 4) { + r = format.charAt(1); r += r; + g = format.charAt(2); g += g; + b = format.charAt(3); b += b; + } else if (format.length == 7) { + r = format.substring(1, 3); + g = format.substring(3, 5); + b = format.substring(5, 7); + } + return pv.rgb(parseInt(r, 16), parseInt(g, 16), parseInt(b, 16), 1); + } + + /* Otherwise, pass-through unsupported colors. */ + return new pv.Color(format, 1); +}; + +/** + * Constructs a color with the specified color format string and opacity. This + * constructor should not be invoked directly; use {@link pv.color} instead. + * + * @class Represents an abstract (possibly translucent) color. The color is + * divided into two parts: the <tt>color</tt> attribute, an opaque color format + * string, and the <tt>opacity</tt> attribute, a float in [0, 1]. The color + * space is dependent on the implementing class; all colors support the + * {@link #rgb} method to convert to RGB color space for interpolation. + * + * <p>See also the <a href="../../api/Color.html">Color guide</a>. + * + * @param {string} color an opaque color format string, such as "#f00". + * @param {number} opacity the opacity, in [0,1]. + * @see pv.color + */ +pv.Color = function(color, opacity) { + /** + * An opaque color format string, such as "#f00". + * + * @type string + * @see <a href="http://www.w3.org/TR/SVG/types.html#ColorKeywords">SVG color + * keywords</a> + * @see <a href="http://www.w3.org/TR/css3-color/">CSS3 color module</a> + */ + this.color = color; + + /** + * The opacity, a float in [0, 1]. + * + * @type number + */ + this.opacity = opacity; +}; + +/** + * Returns a new color that is a brighter version of this color. The behavior of + * this method may vary slightly depending on the underlying color space. + * Although brighter and darker are inverse operations, the results of a series + * of invocations of these two methods might be inconsistent because of rounding + * errors. + * + * @param [k] {number} an optional scale factor; defaults to 1. + * @see #darker + * @returns {pv.Color} a brighter color. + */ +pv.Color.prototype.brighter = function(k) { + return this.rgb().brighter(k); +}; + +/** + * Returns a new color that is a brighter version of this color. The behavior of + * this method may vary slightly depending on the underlying color space. + * Although brighter and darker are inverse operations, the results of a series + * of invocations of these two methods might be inconsistent because of rounding + * errors. + * + * @param [k] {number} an optional scale factor; defaults to 1. + * @see #brighter + * @returns {pv.Color} a darker color. + */ +pv.Color.prototype.darker = function(k) { + return this.rgb().darker(k); +}; + +/** + * Constructs a new RGB color with the specified channel values. + * + * @param {number} r the red channel, an integer in [0,255]. + * @param {number} g the green channel, an integer in [0,255]. + * @param {number} b the blue channel, an integer in [0,255]. + * @param {number} [a] the alpha channel, a float in [0,1]. + * @returns pv.Color.Rgb + */ +pv.rgb = function(r, g, b, a) { + return new pv.Color.Rgb(r, g, b, (arguments.length == 4) ? a : 1); +}; + +/** + * Constructs a new RGB color with the specified channel values. + * + * @class Represents a color in RGB space. + * + * @param {number} r the red channel, an integer in [0,255]. + * @param {number} g the green channel, an integer in [0,255]. + * @param {number} b the blue channel, an integer in [0,255]. + * @param {number} a the alpha channel, a float in [0,1]. + * @extends pv.Color + */ +pv.Color.Rgb = function(r, g, b, a) { + pv.Color.call(this, a ? ("rgb(" + r + "," + g + "," + b + ")") : "none", a); + + /** + * The red channel, an integer in [0, 255]. + * + * @type number + */ + this.r = r; + + /** + * The green channel, an integer in [0, 255]. + * + * @type number + */ + this.g = g; + + /** + * The blue channel, an integer in [0, 255]. + * + * @type number + */ + this.b = b; + + /** + * The alpha channel, a float in [0, 1]. + * + * @type number + */ + this.a = a; +}; +pv.Color.Rgb.prototype = pv.extend(pv.Color); + +/** + * Constructs a new RGB color with the same green, blue and alpha channels as + * this color, with the specified red channel. + * + * @param {number} r the red channel, an integer in [0,255]. + */ +pv.Color.Rgb.prototype.red = function(r) { + return pv.rgb(r, this.g, this.b, this.a); +}; + +/** + * Constructs a new RGB color with the same red, blue and alpha channels as this + * color, with the specified green channel. + * + * @param {number} g the green channel, an integer in [0,255]. + */ +pv.Color.Rgb.prototype.green = function(g) { + return pv.rgb(this.r, g, this.b, this.a); +}; + +/** + * Constructs a new RGB color with the same red, green and alpha channels as + * this color, with the specified blue channel. + * + * @param {number} b the blue channel, an integer in [0,255]. + */ +pv.Color.Rgb.prototype.blue = function(b) { + return pv.rgb(this.r, this.g, b, this.a); +}; + +/** + * Constructs a new RGB color with the same red, green and blue channels as this + * color, with the specified alpha channel. + * + * @param {number} a the alpha channel, a float in [0,1]. + */ +pv.Color.Rgb.prototype.alpha = function(a) { + return pv.rgb(this.r, this.g, this.b, a); +}; + +/** + * Returns the RGB color equivalent to this color. This method is abstract and + * must be implemented by subclasses. + * + * @returns {pv.Color.Rgb} an RGB color. + * @function + * @name pv.Color.prototype.rgb + */ + +/** + * Returns this. + * + * @returns {pv.Color.Rgb} this. + */ +pv.Color.Rgb.prototype.rgb = function() { return this; }; + +/** + * Returns a new color that is a brighter version of this color. This method + * applies an arbitrary scale factor to each of the three RGB components of this + * color to create a brighter version of this color. Although brighter and + * darker are inverse operations, the results of a series of invocations of + * these two methods might be inconsistent because of rounding errors. + * + * @param [k] {number} an optional scale factor; defaults to 1. + * @see #darker + * @returns {pv.Color.Rgb} a brighter color. + */ +pv.Color.Rgb.prototype.brighter = function(k) { + k = Math.pow(0.7, arguments.length ? k : 1); + var r = this.r, g = this.g, b = this.b, i = 30; + if (!r && !g && !b) return pv.rgb(i, i, i, this.a); + if (r && (r < i)) r = i; + if (g && (g < i)) g = i; + if (b && (b < i)) b = i; + return pv.rgb( + Math.min(255, Math.floor(r / k)), + Math.min(255, Math.floor(g / k)), + Math.min(255, Math.floor(b / k)), + this.a); +}; + +/** + * Returns a new color that is a darker version of this color. This method + * applies an arbitrary scale factor to each of the three RGB components of this + * color to create a darker version of this color. Although brighter and darker + * are inverse operations, the results of a series of invocations of these two + * methods might be inconsistent because of rounding errors. + * + * @param [k] {number} an optional scale factor; defaults to 1. + * @see #brighter + * @returns {pv.Color.Rgb} a darker color. + */ +pv.Color.Rgb.prototype.darker = function(k) { + k = Math.pow(0.7, arguments.length ? k : 1); + return pv.rgb( + Math.max(0, Math.floor(k * this.r)), + Math.max(0, Math.floor(k * this.g)), + Math.max(0, Math.floor(k * this.b)), + this.a); +}; + +/** + * Constructs a new HSL color with the specified values. + * + * @param {number} h the hue, an integer in [0, 360]. + * @param {number} s the saturation, a float in [0, 1]. + * @param {number} l the lightness, a float in [0, 1]. + * @param {number} [a] the opacity, a float in [0, 1]. + * @returns pv.Color.Hsl + */ +pv.hsl = function(h, s, l, a) { + return new pv.Color.Hsl(h, s, l, (arguments.length == 4) ? a : 1); +}; + +/** + * Constructs a new HSL color with the specified values. + * + * @class Represents a color in HSL space. + * + * @param {number} h the hue, an integer in [0, 360]. + * @param {number} s the saturation, a float in [0, 1]. + * @param {number} l the lightness, a float in [0, 1]. + * @param {number} a the opacity, a float in [0, 1]. + * @extends pv.Color + */ +pv.Color.Hsl = function(h, s, l, a) { + pv.Color.call(this, "hsl(" + h + "," + (s * 100) + "%," + (l * 100) + "%)", a); + + /** + * The hue, an integer in [0, 360]. + * + * @type number + */ + this.h = h; + + /** + * The saturation, a float in [0, 1]. + * + * @type number + */ + this.s = s; + + /** + * The lightness, a float in [0, 1]. + * + * @type number + */ + this.l = l; + + /** + * The opacity, a float in [0, 1]. + * + * @type number + */ + this.a = a; +}; +pv.Color.Hsl.prototype = pv.extend(pv.Color); + +/** + * Constructs a new HSL color with the same saturation, lightness and alpha as + * this color, and the specified hue. + * + * @param {number} h the hue, an integer in [0, 360]. + */ +pv.Color.Hsl.prototype.hue = function(h) { + return pv.hsl(h, this.s, this.l, this.a); +}; + +/** + * Constructs a new HSL color with the same hue, lightness and alpha as this + * color, and the specified saturation. + * + * @param {number} s the saturation, a float in [0, 1]. + */ +pv.Color.Hsl.prototype.saturation = function(s) { + return pv.hsl(this.h, s, this.l, this.a); +}; + +/** + * Constructs a new HSL color with the same hue, saturation and alpha as this + * color, and the specified lightness. + * + * @param {number} l the lightness, a float in [0, 1]. + */ +pv.Color.Hsl.prototype.lightness = function(l) { + return pv.hsl(this.h, this.s, l, this.a); +}; + +/** + * Constructs a new HSL color with the same hue, saturation and lightness as + * this color, and the specified alpha. + * + * @param {number} a the opacity, a float in [0, 1]. + */ +pv.Color.Hsl.prototype.alpha = function(a) { + return pv.hsl(this.h, this.s, this.l, a); +}; + +/** + * Returns the RGB color equivalent to this HSL color. + * + * @returns {pv.Color.Rgb} an RGB color. + */ +pv.Color.Hsl.prototype.rgb = function() { + var h = this.h, s = this.s, l = this.l; + + /* Some simple corrections for h, s and l. */ + h = h % 360; if (h < 0) h += 360; + s = Math.max(0, Math.min(s, 1)); + l = Math.max(0, Math.min(l, 1)); + + /* From FvD 13.37, CSS Color Module Level 3 */ + var m2 = (l <= .5) ? (l * (1 + s)) : (l + s - l * s); + var m1 = 2 * l - m2; + function v(h) { + if (h > 360) h -= 360; + else if (h < 0) h += 360; + if (h < 60) return m1 + (m2 - m1) * h / 60; + if (h < 180) return m2; + if (h < 240) return m1 + (m2 - m1) * (240 - h) / 60; + return m1; + } + function vv(h) { + return Math.round(v(h) * 255); + } + + return pv.rgb(vv(h + 120), vv(h), vv(h - 120), this.a); +}; + +/** + * @private SVG color keywords, per CSS Color Module Level 3. + * + * @see <a href="http://www.w3.org/TR/SVG/types.html#ColorKeywords">SVG color + * keywords</a> + */ +pv.Color.names = { + aliceblue: "#f0f8ff", + antiquewhite: "#faebd7", + aqua: "#00ffff", + aquamarine: "#7fffd4", + azure: "#f0ffff", + beige: "#f5f5dc", + bisque: "#ffe4c4", + black: "#000000", + blanchedalmond: "#ffebcd", + blue: "#0000ff", + blueviolet: "#8a2be2", + brown: "#a52a2a", + burlywood: "#deb887", + cadetblue: "#5f9ea0", + chartreuse: "#7fff00", + chocolate: "#d2691e", + coral: "#ff7f50", + cornflowerblue: "#6495ed", + cornsilk: "#fff8dc", + crimson: "#dc143c", + cyan: "#00ffff", + darkblue: "#00008b", + darkcyan: "#008b8b", + darkgoldenrod: "#b8860b", + darkgray: "#a9a9a9", + darkgreen: "#006400", + darkgrey: "#a9a9a9", + darkkhaki: "#bdb76b", + darkmagenta: "#8b008b", + darkolivegreen: "#556b2f", + darkorange: "#ff8c00", + darkorchid: "#9932cc", + darkred: "#8b0000", + darksalmon: "#e9967a", + darkseagreen: "#8fbc8f", + darkslateblue: "#483d8b", + darkslategray: "#2f4f4f", + darkslategrey: "#2f4f4f", + darkturquoise: "#00ced1", + darkviolet: "#9400d3", + deeppink: "#ff1493", + deepskyblue: "#00bfff", + dimgray: "#696969", + dimgrey: "#696969", + dodgerblue: "#1e90ff", + firebrick: "#b22222", + floralwhite: "#fffaf0", + forestgreen: "#228b22", + fuchsia: "#ff00ff", + gainsboro: "#dcdcdc", + ghostwhite: "#f8f8ff", + gold: "#ffd700", + goldenrod: "#daa520", + gray: "#808080", + green: "#008000", + greenyellow: "#adff2f", + grey: "#808080", + honeydew: "#f0fff0", + hotpink: "#ff69b4", + indianred: "#cd5c5c", + indigo: "#4b0082", + ivory: "#fffff0", + khaki: "#f0e68c", + lavender: "#e6e6fa", + lavenderblush: "#fff0f5", + lawngreen: "#7cfc00", + lemonchiffon: "#fffacd", + lightblue: "#add8e6", + lightcoral: "#f08080", + lightcyan: "#e0ffff", + lightgoldenrodyellow: "#fafad2", + lightgray: "#d3d3d3", + lightgreen: "#90ee90", + lightgrey: "#d3d3d3", + lightpink: "#ffb6c1", + lightsalmon: "#ffa07a", + lightseagreen: "#20b2aa", + lightskyblue: "#87cefa", + lightslategray: "#778899", + lightslategrey: "#778899", + lightsteelblue: "#b0c4de", + lightyellow: "#ffffe0", + lime: "#00ff00", + limegreen: "#32cd32", + linen: "#faf0e6", + magenta: "#ff00ff", + maroon: "#800000", + mediumaquamarine: "#66cdaa", + mediumblue: "#0000cd", + mediumorchid: "#ba55d3", + mediumpurple: "#9370db", + mediumseagreen: "#3cb371", + mediumslateblue: "#7b68ee", + mediumspringgreen: "#00fa9a", + mediumturquoise: "#48d1cc", + mediumvioletred: "#c71585", + midnightblue: "#191970", + mintcream: "#f5fffa", + mistyrose: "#ffe4e1", + moccasin: "#ffe4b5", + navajowhite: "#ffdead", + navy: "#000080", + oldlace: "#fdf5e6", + olive: "#808000", + olivedrab: "#6b8e23", + orange: "#ffa500", + orangered: "#ff4500", + orchid: "#da70d6", + palegoldenrod: "#eee8aa", + palegreen: "#98fb98", + paleturquoise: "#afeeee", + palevioletred: "#db7093", + papayawhip: "#ffefd5", + peachpuff: "#ffdab9", + peru: "#cd853f", + pink: "#ffc0cb", + plum: "#dda0dd", + powderblue: "#b0e0e6", + purple: "#800080", + red: "#ff0000", + rosybrown: "#bc8f8f", + royalblue: "#4169e1", + saddlebrown: "#8b4513", + salmon: "#fa8072", + sandybrown: "#f4a460", + seagreen: "#2e8b57", + seashell: "#fff5ee", + sienna: "#a0522d", + silver: "#c0c0c0", + skyblue: "#87ceeb", + slateblue: "#6a5acd", + slategray: "#708090", + slategrey: "#708090", + snow: "#fffafa", + springgreen: "#00ff7f", + steelblue: "#4682b4", + tan: "#d2b48c", + teal: "#008080", + thistle: "#d8bfd8", + tomato: "#ff6347", + turquoise: "#40e0d0", + violet: "#ee82ee", + wheat: "#f5deb3", + white: "#ffffff", + whitesmoke: "#f5f5f5", + yellow: "#ffff00", + yellowgreen: "#9acd32", + transparent: pv.Color.transparent = pv.rgb(0, 0, 0, 0) +}; + +/* Initialized named colors. */ +(function() { + var names = pv.Color.names; + for (var name in names) names[name] = pv.color(names[name]); +})(); +/** + * Returns a new categorical color encoding using the specified colors. The + * arguments to this method are an array of colors; see {@link pv.color}. For + * example, to create a categorical color encoding using the <tt>species</tt> + * attribute: + * + * <pre>pv.colors("red", "green", "blue").by(function(d) d.species)</pre> + * + * The result of this expression can be used as a fill- or stroke-style + * property. This assumes that the data's <tt>species</tt> attribute is a + * string. + * + * @param {string} colors... categorical colors. + * @see pv.Scale.ordinal + * @returns {pv.Scale.ordinal} an ordinal color scale. + */ +pv.colors = function() { + var scale = pv.Scale.ordinal(); + scale.range.apply(scale, arguments); + return scale; +}; + +/** + * A collection of standard color palettes for categorical encoding. + * + * @namespace A collection of standard color palettes for categorical encoding. + */ +pv.Colors = {}; + +/** + * Returns a new 10-color scheme. The arguments to this constructor are + * optional, and equivalent to calling {@link pv.Scale.OrdinalScale#domain}. The + * following colors are used: + * + * <div style="background:#1f77b4;">#1f77b4</div> + * <div style="background:#ff7f0e;">#ff7f0e</div> + * <div style="background:#2ca02c;">#2ca02c</div> + * <div style="background:#d62728;">#d62728</div> + * <div style="background:#9467bd;">#9467bd</div> + * <div style="background:#8c564b;">#8c564b</div> + * <div style="background:#e377c2;">#e377c2</div> + * <div style="background:#7f7f7f;">#7f7f7f</div> + * <div style="background:#bcbd22;">#bcbd22</div> + * <div style="background:#17becf;">#17becf</div> + * + * @param {number...} domain... domain values. + * @returns {pv.Scale.ordinal} a new ordinal color scale. + * @see pv.color + */ +pv.Colors.category10 = function() { + var scale = pv.colors( + "#1f77b4", "#ff7f0e", "#2ca02c", "#d62728", "#9467bd", + "#8c564b", "#e377c2", "#7f7f7f", "#bcbd22", "#17becf"); + scale.domain.apply(scale, arguments); + return scale; +}; + +/** + * Returns a new 20-color scheme. The arguments to this constructor are + * optional, and equivalent to calling {@link pv.Scale.OrdinalScale#domain}. The + * following colors are used: + * + * <div style="background:#1f77b4;">#1f77b4</div> + * <div style="background:#aec7e8;">#aec7e8</div> + * <div style="background:#ff7f0e;">#ff7f0e</div> + * <div style="background:#ffbb78;">#ffbb78</div> + * <div style="background:#2ca02c;">#2ca02c</div> + * <div style="background:#98df8a;">#98df8a</div> + * <div style="background:#d62728;">#d62728</div> + * <div style="background:#ff9896;">#ff9896</div> + * <div style="background:#9467bd;">#9467bd</div> + * <div style="background:#c5b0d5;">#c5b0d5</div> + * <div style="background:#8c564b;">#8c564b</div> + * <div style="background:#c49c94;">#c49c94</div> + * <div style="background:#e377c2;">#e377c2</div> + * <div style="background:#f7b6d2;">#f7b6d2</div> + * <div style="background:#7f7f7f;">#7f7f7f</div> + * <div style="background:#c7c7c7;">#c7c7c7</div> + * <div style="background:#bcbd22;">#bcbd22</div> + * <div style="background:#dbdb8d;">#dbdb8d</div> + * <div style="background:#17becf;">#17becf</div> + * <div style="background:#9edae5;">#9edae5</div> + * + * @param {number...} domain... domain values. + * @returns {pv.Scale.ordinal} a new ordinal color scale. + * @see pv.color +*/ +pv.Colors.category20 = function() { + var scale = pv.colors( + "#1f77b4", "#aec7e8", "#ff7f0e", "#ffbb78", "#2ca02c", + "#98df8a", "#d62728", "#ff9896", "#9467bd", "#c5b0d5", + "#8c564b", "#c49c94", "#e377c2", "#f7b6d2", "#7f7f7f", + "#c7c7c7", "#bcbd22", "#dbdb8d", "#17becf", "#9edae5"); + scale.domain.apply(scale, arguments); + return scale; +}; + +/** + * Returns a new alternative 19-color scheme. The arguments to this constructor + * are optional, and equivalent to calling + * {@link pv.Scale.OrdinalScale#domain}. The following colors are used: + * + * <div style="background:#9c9ede;">#9c9ede</div> + * <div style="background:#7375b5;">#7375b5</div> + * <div style="background:#4a5584;">#4a5584</div> + * <div style="background:#cedb9c;">#cedb9c</div> + * <div style="background:#b5cf6b;">#b5cf6b</div> + * <div style="background:#8ca252;">#8ca252</div> + * <div style="background:#637939;">#637939</div> + * <div style="background:#e7cb94;">#e7cb94</div> + * <div style="background:#e7ba52;">#e7ba52</div> + * <div style="background:#bd9e39;">#bd9e39</div> + * <div style="background:#8c6d31;">#8c6d31</div> + * <div style="background:#e7969c;">#e7969c</div> + * <div style="background:#d6616b;">#d6616b</div> + * <div style="background:#ad494a;">#ad494a</div> + * <div style="background:#843c39;">#843c39</div> + * <div style="background:#de9ed6;">#de9ed6</div> + * <div style="background:#ce6dbd;">#ce6dbd</div> + * <div style="background:#a55194;">#a55194</div> + * <div style="background:#7b4173;">#7b4173</div> + * + * @param {number...} domain... domain values. + * @returns {pv.Scale.ordinal} a new ordinal color scale. + * @see pv.color + */ +pv.Colors.category19 = function() { + var scale = pv.colors( + "#9c9ede", "#7375b5", "#4a5584", "#cedb9c", "#b5cf6b", + "#8ca252", "#637939", "#e7cb94", "#e7ba52", "#bd9e39", + "#8c6d31", "#e7969c", "#d6616b", "#ad494a", "#843c39", + "#de9ed6", "#ce6dbd", "#a55194", "#7b4173"); + scale.domain.apply(scale, arguments); + return scale; +}; +/** + * Returns a linear color ramp from the specified <tt>start</tt> color to the + * specified <tt>end</tt> color. The color arguments may be specified either as + * <tt>string</tt>s or as {@link pv.Color}s. This is equivalent to: + * + * <pre> pv.Scale.linear().domain(0, 1).range(...)</pre> + * + * @param {string} start the start color; may be a <tt>pv.Color</tt>. + * @param {string} end the end color; may be a <tt>pv.Color</tt>. + * @returns {Function} a color ramp from <tt>start</tt> to <tt>end</tt>. + * @see pv.Scale.linear + */ +pv.ramp = function(start, end) { + var scale = pv.Scale.linear(); + scale.range.apply(scale, arguments); + return scale; +}; +/** + * @private + * @namespace + */ +pv.Scene = pv.SvgScene = { + /* Various namespaces. */ + svg: "http://www.w3.org/2000/svg", + xmlns: "http://www.w3.org/2000/xmlns", + xlink: "http://www.w3.org/1999/xlink", + xhtml: "http://www.w3.org/1999/xhtml", + + /** The pre-multipled scale, based on any enclosing transforms. */ + scale: 1, + + /** The set of supported events. */ + events: [ + "DOMMouseScroll", // for Firefox + "mousewheel", + "mousedown", + "mouseup", + "mouseover", + "mouseout", + "mousemove", + "click", + "dblclick" + ], + + /** Implicit values for SVG and CSS properties. */ + implicit: { + svg: { + "shape-rendering": "auto", + "pointer-events": "painted", + "x": 0, + "y": 0, + "dy": 0, + "text-anchor": "start", + "transform": "translate(0,0)", + "fill": "none", + "fill-opacity": 1, + "stroke": "none", + "stroke-opacity": 1, + "stroke-width": 1.5, + "stroke-linejoin": "miter" + }, + css: { + "font": "10px sans-serif" + } + } +}; + +/** + * Updates the display for the specified array of scene nodes. + * + * @param scenes {array} an array of scene nodes. + */ +pv.SvgScene.updateAll = function(scenes) { + if (scenes.length + && scenes[0].reverse + && (scenes.type != "line") + && (scenes.type != "area")) { + var reversed = pv.extend(scenes); + for (var i = 0, j = scenes.length - 1; j >= 0; i++, j--) { + reversed[i] = scenes[j]; + } + scenes = reversed; + } + this.removeSiblings(this[scenes.type](scenes)); +}; + +/** + * Creates a new SVG element of the specified type. + * + * @param type {string} an SVG element type, such as "rect". + * @returns a new SVG element. + */ +pv.SvgScene.create = function(type) { + return document.createElementNS(this.svg, type); +}; + +/** + * Expects the element <i>e</i> to be the specified type. If the element does + * not exist, a new one is created. If the element does exist but is the wrong + * type, it is replaced with the specified element. + * + * @param e the current SVG element. + * @param type {string} an SVG element type, such as "rect". + * @param attributes an optional attribute map. + * @param style an optional style map. + * @returns a new SVG element. + */ +pv.SvgScene.expect = function(e, type, attributes, style) { + if (e) { + if (e.tagName == "a") e = e.firstChild; + if (e.tagName != type) { + var n = this.create(type); + e.parentNode.replaceChild(n, e); + e = n; + } + } else { + e = this.create(type); + } + for (var name in attributes) { + var value = attributes[name]; + if (value == this.implicit.svg[name]) value = null; + if (value == null) e.removeAttribute(name); + else e.setAttribute(name, value); + } + for (var name in style) { + var value = style[name]; + if (value == this.implicit.css[name]) value = null; + if (value == null) e.style.removeProperty(name); + else e.style[name] = value; + } + return e; +}; + +/** TODO */ +pv.SvgScene.append = function(e, scenes, index) { + e.$scene = {scenes:scenes, index:index}; + e = this.title(e, scenes[index]); + if (!e.parentNode) scenes.$g.appendChild(e); + return e.nextSibling; +}; + +/** + * Applies a title tooltip to the specified element <tt>e</tt>, using the + * <tt>title</tt> property of the specified scene node <tt>s</tt>. Note that + * this implementation does not create an SVG <tt>title</tt> element as a child + * of <tt>e</tt>; although this is the recommended standard, it is only + * supported in Opera. Instead, an anchor element is created around the element + * <tt>e</tt>, and the <tt>xlink:title</tt> attribute is set accordingly. + * + * @param e an SVG element. + * @param s a scene node. + */ +pv.SvgScene.title = function(e, s) { + var a = e.parentNode; + if (a && (a.tagName != "a")) a = null; + if (s.title) { + if (!a) { + a = this.create("a"); + if (e.parentNode) e.parentNode.replaceChild(a, e); + a.appendChild(e); + } + a.setAttributeNS(this.xlink, "title", s.title); + return a; + } + if (a) a.parentNode.replaceChild(e, a); + return e; +}; + +/** TODO */ +pv.SvgScene.dispatch = pv.listener(function(e) { + var t = e.target.$scene; + if (t) { + var type = e.type; + + /* Fixes for mousewheel support on Firefox & Opera. */ + switch (type) { + case "DOMMouseScroll": { + type = "mousewheel"; + e.wheel = -480 * e.detail; + break; + } + case "mousewheel": { + e.wheel = (window.opera ? 12 : 1) * e.wheelDelta; + break; + } + } + + if (pv.Mark.dispatch(type, t.scenes, t.index)) e.preventDefault(); + } +}); + +/** @private Remove siblings following element <i>e</i>. */ +pv.SvgScene.removeSiblings = function(e) { + while (e) { + var n = e.nextSibling; + e.parentNode.removeChild(e); + e = n; + } +}; + +/** @private Do nothing when rendering undefined mark types. */ +pv.SvgScene.undefined = function() {}; +/** + * @private Converts the specified b-spline curve segment to a bezier curve + * compatible with SVG "C". + * + * @param p0 the first control point. + * @param p1 the second control point. + * @param p2 the third control point. + * @param p3 the fourth control point. + */ +pv.SvgScene.pathBasis = (function() { + + /** + * Matrix to transform basis (b-spline) control points to bezier control + * points. Derived from FvD 11.2.8. + */ + var basis = [ + [ 1/6, 2/3, 1/6, 0 ], + [ 0, 2/3, 1/3, 0 ], + [ 0, 1/3, 2/3, 0 ], + [ 0, 1/6, 2/3, 1/6 ] + ]; + + /** + * Returns the point that is the weighted sum of the specified control points, + * using the specified weights. This method requires that there are four + * weights and four control points. + */ + function weight(w, p0, p1, p2, p3) { + return { + x: w[0] * p0.left + w[1] * p1.left + w[2] * p2.left + w[3] * p3.left, + y: w[0] * p0.top + w[1] * p1.top + w[2] * p2.top + w[3] * p3.top + }; + } + + var convert = function(p0, p1, p2, p3) { + var b1 = weight(basis[1], p0, p1, p2, p3), + b2 = weight(basis[2], p0, p1, p2, p3), + b3 = weight(basis[3], p0, p1, p2, p3); + return "C" + b1.x + "," + b1.y + + "," + b2.x + "," + b2.y + + "," + b3.x + "," + b3.y; + }; + + convert.segment = function(p0, p1, p2, p3) { + var b0 = weight(basis[0], p0, p1, p2, p3), + b1 = weight(basis[1], p0, p1, p2, p3), + b2 = weight(basis[2], p0, p1, p2, p3), + b3 = weight(basis[3], p0, p1, p2, p3); + return "M" + b0.x + "," + b0.y + + "C" + b1.x + "," + b1.y + + "," + b2.x + "," + b2.y + + "," + b3.x + "," + b3.y; + }; + + return convert; +})(); + +/** + * @private Interpolates the given points using the basis spline interpolation. + * Returns an SVG path without the leading M instruction to allow path + * appending. + * + * @param points the array of points. + */ +pv.SvgScene.curveBasis = function(points) { + if (points.length <= 2) return ""; + var path = "", + p0 = points[0], + p1 = p0, + p2 = p0, + p3 = points[1]; + path += this.pathBasis(p0, p1, p2, p3); + for (var i = 2; i < points.length; i++) { + p0 = p1; + p1 = p2; + p2 = p3; + p3 = points[i]; + path += this.pathBasis(p0, p1, p2, p3); + } + /* Cycle through to get the last point. */ + path += this.pathBasis(p1, p2, p3, p3); + path += this.pathBasis(p2, p3, p3, p3); + return path; +}; + +/** + * @private Interpolates the given points using the basis spline interpolation. + * If points.length == tangents.length then a regular Hermite interpolation is + * performed, if points.length == tangents.length + 2 then the first and last + * segments are filled in with cubic bazier segments. Returns an array of path + * strings. + * + * @param points the array of points. + */ +pv.SvgScene.curveBasisSegments = function(points) { + if (points.length <= 2) return ""; + var paths = [], + p0 = points[0], + p1 = p0, + p2 = p0, + p3 = points[1], + firstPath = this.pathBasis.segment(p0, p1, p2, p3); + + p0 = p1; + p1 = p2; + p2 = p3; + p3 = points[2]; + paths.push(firstPath + this.pathBasis(p0, p1, p2, p3)); // merge first & second path + for (var i = 3; i < points.length; i++) { + p0 = p1; + p1 = p2; + p2 = p3; + p3 = points[i]; + paths.push(this.pathBasis.segment(p0, p1, p2, p3)); + } + + // merge last & second-to-last path + paths.push(this.pathBasis.segment(p1, p2, p3, p3) + this.pathBasis(p2, p3, p3, p3)); + return paths; +}; + +/** + * @private Interpolates the given points with respective tangents using the cubic + * Hermite spline interpolation. If points.length == tangents.length then a regular + * Hermite interpolation is performed, if points.length == tangents.length + 2 then + * the first and last segments are filled in with cubic bazier segments. + * Returns an SVG path without the leading M instruction to allow path appending. + * + * @param points the array of points. + * @param tangents the array of tangent vectors. + */ +pv.SvgScene.curveHermite = function(points, tangents) { + if (tangents.length < 1 + || (points.length != tangents.length + && points.length != tangents.length + 2)) return ""; + var quad = points.length != tangents.length, + path = "", + p0 = points[0], + p = points[1], + t0 = tangents[0], + t = t0, + pi = 1; + + if (quad) { + path += "Q" + (p.left - t0.x * 2 / 3) + "," + (p.top - t0.y * 2 / 3) + + "," + p.left + "," + p.top; + p0 = points[1]; + pi = 2; + } + + if (tangents.length > 1) { + t = tangents[1]; + p = points[pi]; + pi++; + path += "C" + (p0.left + t0.x) + "," + (p0.top + t0.y) + + "," + (p.left - t.x) + "," + (p.top - t.y) + + "," + p.left + "," + p.top; + for (var i = 2; i < tangents.length; i++, pi++) { + p = points[pi]; + t = tangents[i]; + path += "S" + (p.left - t.x) + "," + (p.top - t.y) + + "," + p.left + "," + p.top; + } + } + + if (quad) { + var lp = points[pi]; + path += "Q" + (p.left + t.x * 2 / 3) + "," + (p.top + t.y * 2 / 3) + "," + + lp.left + "," + lp.top; + } + + return path; +}; + +/** + * @private Interpolates the given points with respective tangents using the + * cubic Hermite spline interpolation. Returns an array of path strings. + * + * @param points the array of points. + * @param tangents the array of tangent vectors. + */ +pv.SvgScene.curveHermiteSegments = function(points, tangents) { + if (tangents.length < 1 + || (points.length != tangents.length + && points.length != tangents.length + 2)) return []; + var quad = points.length != tangents.length, + paths = [], + p0 = points[0], + p = p0, + t0 = tangents[0], + t = t0, + pi = 1; + + if (quad) { + p = points[1]; + paths.push("M" + p0.left + "," + p0.top + + "Q" + (p.left - t.x * 2 / 3) + "," + (p.top - t.y * 2 / 3) + + "," + p.left + "," + p.top); + pi = 2; + } + + for (var i = 1; i < tangents.length; i++, pi++) { + p0 = p; + t0 = t; + p = points[pi]; + t = tangents[i]; + paths.push("M" + p0.left + "," + p0.top + + "C" + (p0.left + t0.x) + "," + (p0.top + t0.y) + + "," + (p.left - t.x) + "," + (p.top - t.y) + + "," + p.left + "," + p.top); + } + + if (quad) { + var lp = points[pi]; + paths.push("M" + p.left + "," + p.top + + "Q" + (p.left + t.x * 2 / 3) + "," + (p.top + t.y * 2 / 3) + "," + + lp.left + "," + lp.top); + } + + return paths; +}; + +/** + * @private Computes the tangents for the given points needed for cardinal + * spline interpolation. Returns an array of tangent vectors. Note: that for n + * points only the n-2 well defined tangents are returned. + * + * @param points the array of points. + * @param tension the tension of hte cardinal spline. + */ +pv.SvgScene.cardinalTangents = function(points, tension) { + var tangents = [], + a = (1 - tension) / 2, + p0 = points[0], + p1 = points[1], + p2 = points[2]; + + for (var i = 3; i < points.length; i++) { + tangents.push({x: a * (p2.left - p0.left), y: a * (p2.top - p0.top)}); + p0 = p1; + p1 = p2; + p2 = points[i]; + } + + tangents.push({x: a * (p2.left - p0.left), y: a * (p2.top - p0.top)}); + return tangents; +}; + +/** + * @private Interpolates the given points using cardinal spline interpolation. + * Returns an SVG path without the leading M instruction to allow path + * appending. + * + * @param points the array of points. + * @param tension the tension of hte cardinal spline. + */ +pv.SvgScene.curveCardinal = function(points, tension) { + if (points.length <= 2) return ""; + return this.curveHermite(points, this.cardinalTangents(points, tension)); +}; + +/** + * @private Interpolates the given points using cardinal spline interpolation. + * Returns an array of path strings. + * + * @param points the array of points. + * @param tension the tension of hte cardinal spline. + */ +pv.SvgScene.curveCardinalSegments = function(points, tension) { + if (points.length <= 2) return ""; + return this.curveHermiteSegments(points, this.cardinalTangents(points, tension)); +}; + +/** + * @private Interpolates the given points using Fritsch-Carlson Monotone cubic + * Hermite interpolation. Returns an array of tangent vectors. + * + * @param points the array of points. + */ +pv.SvgScene.monotoneTangents = function(points) { + var tangents = [], + d = [], + m = [], + dx = [], + k = 0; + + /* Compute the slopes of the secant lines between successive points. */ + for (k = 0; k < points.length-1; k++) { + d[k] = (points[k+1].top - points[k].top)/(points[k+1].left - points[k].left); + } + + /* Initialize the tangents at every point as the average of the secants. */ + m[0] = d[0]; + dx[0] = points[1].left - points[0].left; + for (k = 1; k < points.length - 1; k++) { + m[k] = (d[k-1]+d[k])/2; + dx[k] = (points[k+1].left - points[k-1].left)/2; + } + m[k] = d[k-1]; + dx[k] = (points[k].left - points[k-1].left); + + /* Step 3. Very important, step 3. Yep. Wouldn't miss it. */ + for (k = 0; k < points.length - 1; k++) { + if (d[k] == 0) { + m[ k ] = 0; + m[k+1] = 0; + } + } + + /* Step 4 + 5. Out of 5 or more steps. */ + for (k = 0; k < points.length - 1; k++) { + if ((Math.abs(m[k]) < 1e-5) || (Math.abs(m[k+1]) < 1e-5)) continue; + var ak = m[k] / d[k], + bk = m[k + 1] / d[k], + s = ak * ak + bk * bk; // monotone constant (?) + if (s > 9) { + var tk = 3 / Math.sqrt(s); + m[k] = tk * ak * d[k]; + m[k + 1] = tk * bk * d[k]; + } + } + + var len; + for (var i = 0; i < points.length; i++) { + len = 1 + m[i] * m[i]; // pv.vector(1, m[i]).norm().times(dx[i]/3) + tangents.push({x: dx[i] / 3 / len, y: m[i] * dx[i] / 3 / len}); + } + + return tangents; +}; + +/** + * @private Interpolates the given points using Fritsch-Carlson Monotone cubic + * Hermite interpolation. Returns an SVG path without the leading M instruction + * to allow path appending. + * + * @param points the array of points. + */ +pv.SvgScene.curveMonotone = function(points) { + if (points.length <= 2) return ""; + return this.curveHermite(points, this.monotoneTangents(points)); +} + +/** + * @private Interpolates the given points using Fritsch-Carlson Monotone cubic + * Hermite interpolation. + * Returns an array of path strings. + * + * @param points the array of points. + */ +pv.SvgScene.curveMonotoneSegments = function(points) { + if (points.length <= 2) return ""; + return this.curveHermiteSegments(points, this.monotoneTangents(points)); +}; +pv.SvgScene.area = function(scenes) { + var e = scenes.$g.firstChild; + if (!scenes.length) return e; + var s = scenes[0]; + + /* segmented */ + if (s.segmented) return this.areaSegment(scenes); + + /* visible */ + if (!s.visible) return e; + var fill = s.fillStyle, stroke = s.strokeStyle; + if (!fill.opacity && !stroke.opacity) return e; + + /** @private Computes the straight path for the range [i, j]. */ + function path(i, j) { + var p1 = [], p2 = []; + for (var k = j; i <= k; i++, j--) { + var si = scenes[i], + sj = scenes[j], + pi = si.left + "," + si.top, + pj = (sj.left + sj.width) + "," + (sj.top + sj.height); + + /* interpolate */ + if (i < k) { + var sk = scenes[i + 1], sl = scenes[j - 1]; + switch (s.interpolate) { + case "step-before": { + pi += "V" + sk.top; + pj += "H" + (sl.left + sl.width); + break; + } + case "step-after": { + pi += "H" + sk.left; + pj += "V" + (sl.top + sl.height); + break; + } + } + } + + p1.push(pi); + p2.push(pj); + } + return p1.concat(p2).join("L"); + } + + /** @private Computes the curved path for the range [i, j]. */ + function pathCurve(i, j) { + var pointsT = [], pointsB = [], pathT, pathB; + + for (var k = j; i <= k; i++, j--) { + var sj = scenes[j]; + pointsT.push(scenes[i]); + pointsB.push({left: sj.left + sj.width, top: sj.top + sj.height}); + } + + if (s.interpolate == "basis") { + pathT = pv.SvgScene.curveBasis(pointsT); + pathB = pv.SvgScene.curveBasis(pointsB); + } else if (s.interpolate == "cardinal") { + pathT = pv.SvgScene.curveCardinal(pointsT, s.tension); + pathB = pv.SvgScene.curveCardinal(pointsB, s.tension); + } else { // monotone + pathT = pv.SvgScene.curveMonotone(pointsT); + pathB = pv.SvgScene.curveMonotone(pointsB); + } + + return pointsT[0].left + "," + pointsT[0].top + pathT + + "L" + pointsB[0].left + "," + pointsB[0].top + pathB; + } + + /* points */ + var d = [], si, sj; + for (var i = 0; i < scenes.length; i++) { + si = scenes[i]; if (!si.width && !si.height) continue; + for (var j = i + 1; j < scenes.length; j++) { + sj = scenes[j]; if (!sj.width && !sj.height) break; + } + if (i && (s.interpolate != "step-after")) i--; + if ((j < scenes.length) && (s.interpolate != "step-before")) j++; + d.push(((j - i > 2 + && (s.interpolate == "basis" + || s.interpolate == "cardinal" + || s.interpolate == "monotone")) + ? pathCurve : path)(i, j - 1)); + i = j - 1; + } + if (!d.length) return e; + + e = this.expect(e, "path", { + "shape-rendering": s.antialias ? null : "crispEdges", + "pointer-events": s.events, + "cursor": s.cursor, + "d": "M" + d.join("ZM") + "Z", + "fill": fill.color, + "fill-opacity": fill.opacity || null, + "stroke": stroke.color, + "stroke-opacity": stroke.opacity || null, + "stroke-width": stroke.opacity ? s.lineWidth / this.scale : null + }); + return this.append(e, scenes, 0); +}; + +pv.SvgScene.areaSegment = function(scenes) { + var e = scenes.$g.firstChild, s = scenes[0], pathsT, pathsB; + if (s.interpolate == "basis" + || s.interpolate == "cardinal" + || s.interpolate == "monotone") { + var pointsT = [], pointsB = []; + + for (var i = 0, n = scenes.length; i < n; i++) { + var sj = scenes[n - i - 1]; + pointsT.push(scenes[i]); + pointsB.push({left: sj.left + sj.width, top: sj.top + sj.height}); + } + + if (s.interpolate == "basis") { + pathsT = this.curveBasisSegments(pointsT); + pathsB = this.curveBasisSegments(pointsB); + } else if (s.interpolate == "cardinal") { + pathsT = this.curveCardinalSegments(pointsT, s.tension); + pathsB = this.curveCardinalSegments(pointsB, s.tension); + } else { // monotone + pathsT = this.curveMonotoneSegments(pointsT); + pathsB = this.curveMonotoneSegments(pointsB); + } + } + + for (var i = 0, n = scenes.length - 1; i < n; i++) { + var s1 = scenes[i], s2 = scenes[i + 1]; + + /* visible */ + if (!s1.visible || !s2.visible) continue; + var fill = s1.fillStyle, stroke = s1.strokeStyle; + if (!fill.opacity && !stroke.opacity) continue; + + var d; + if (pathsT) { + var pathT = pathsT[i], + pathB = "L" + pathsB[n - i - 1].substr(1); + + d = pathT + pathB + "Z"; + } else { + /* interpolate */ + var si = s1, sj = s2; + switch (s1.interpolate) { + case "step-before": si = s2; break; + case "step-after": sj = s1; break; + } + + /* path */ + d = "M" + s1.left + "," + si.top + + "L" + s2.left + "," + sj.top + + "L" + (s2.left + s2.width) + "," + (sj.top + sj.height) + + "L" + (s1.left + s1.width) + "," + (si.top + si.height) + + "Z"; + } + + e = this.expect(e, "path", { + "shape-rendering": s1.antialias ? null : "crispEdges", + "pointer-events": s1.events, + "cursor": s1.cursor, + "d": d, + "fill": fill.color, + "fill-opacity": fill.opacity || null, + "stroke": stroke.color, + "stroke-opacity": stroke.opacity || null, + "stroke-width": stroke.opacity ? s1.lineWidth / this.scale : null + }); + e = this.append(e, scenes, i); + } + return e; +}; +pv.SvgScene.bar = function(scenes) { + var e = scenes.$g.firstChild; + for (var i = 0; i < scenes.length; i++) { + var s = scenes[i]; + + /* visible */ + if (!s.visible) continue; + var fill = s.fillStyle, stroke = s.strokeStyle; + if (!fill.opacity && !stroke.opacity) continue; + + e = this.expect(e, "rect", { + "shape-rendering": s.antialias ? null : "crispEdges", + "pointer-events": s.events, + "cursor": s.cursor, + "x": s.left, + "y": s.top, + "width": Math.max(1E-10, s.width), + "height": Math.max(1E-10, s.height), + "fill": fill.color, + "fill-opacity": fill.opacity || null, + "stroke": stroke.color, + "stroke-opacity": stroke.opacity || null, + "stroke-width": stroke.opacity ? s.lineWidth / this.scale : null + }); + e = this.append(e, scenes, i); + } + return e; +}; +pv.SvgScene.dot = function(scenes) { + var e = scenes.$g.firstChild; + for (var i = 0; i < scenes.length; i++) { + var s = scenes[i]; + + /* visible */ + if (!s.visible) continue; + var fill = s.fillStyle, stroke = s.strokeStyle; + if (!fill.opacity && !stroke.opacity) continue; + + /* points */ + var radius = s.radius, path = null; + switch (s.shape) { + case "cross": { + path = "M" + -radius + "," + -radius + + "L" + radius + "," + radius + + "M" + radius + "," + -radius + + "L" + -radius + "," + radius; + break; + } + case "triangle": { + var h = radius, w = radius * 1.1547; // 2 / Math.sqrt(3) + path = "M0," + h + + "L" + w +"," + -h + + " " + -w + "," + -h + + "Z"; + break; + } + case "diamond": { + radius *= Math.SQRT2; + path = "M0," + -radius + + "L" + radius + ",0" + + " 0," + radius + + " " + -radius + ",0" + + "Z"; + break; + } + case "square": { + path = "M" + -radius + "," + -radius + + "L" + radius + "," + -radius + + " " + radius + "," + radius + + " " + -radius + "," + radius + + "Z"; + break; + } + case "tick": { + path = "M0,0L0," + -s.size; + break; + } + case "bar": { + path = "M0," + (s.size / 2) + "L0," + -(s.size / 2); + break; + } + } + + /* Use <circle> for circles, <path> for everything else. */ + var svg = { + "shape-rendering": s.antialias ? null : "crispEdges", + "pointer-events": s.events, + "cursor": s.cursor, + "fill": fill.color, + "fill-opacity": fill.opacity || null, + "stroke": stroke.color, + "stroke-opacity": stroke.opacity || null, + "stroke-width": stroke.opacity ? s.lineWidth / this.scale : null + }; + if (path) { + svg.transform = "translate(" + s.left + "," + s.top + ")"; + if (s.angle) svg.transform += " rotate(" + 180 * s.angle / Math.PI + ")"; + svg.d = path; + e = this.expect(e, "path", svg); + } else { + svg.cx = s.left; + svg.cy = s.top; + svg.r = radius; + e = this.expect(e, "circle", svg); + } + e = this.append(e, scenes, i); + } + return e; +}; +pv.SvgScene.image = function(scenes) { + var e = scenes.$g.firstChild; + for (var i = 0; i < scenes.length; i++) { + var s = scenes[i]; + + /* visible */ + if (!s.visible) continue; + + /* fill */ + e = this.fill(e, scenes, i); + + /* image */ + if (s.image) { + e = this.expect(e, "foreignObject", { + "cursor": s.cursor, + "x": s.left, + "y": s.top, + "width": s.width, + "height": s.height + }); + var c = e.firstChild || e.appendChild(document.createElementNS(this.xhtml, "canvas")); + c.$scene = {scenes:scenes, index:i}; + c.style.width = s.width; + c.style.height = s.height; + c.width = s.imageWidth; + c.height = s.imageHeight; + c.getContext("2d").putImageData(s.image, 0, 0); + } else { + e = this.expect(e, "image", { + "preserveAspectRatio": "none", + "cursor": s.cursor, + "x": s.left, + "y": s.top, + "width": s.width, + "height": s.height + }); + e.setAttributeNS(this.xlink, "href", s.url); + } + e = this.append(e, scenes, i); + + /* stroke */ + e = this.stroke(e, scenes, i); + } + return e; +}; +pv.SvgScene.label = function(scenes) { + var e = scenes.$g.firstChild; + for (var i = 0; i < scenes.length; i++) { + var s = scenes[i]; + + /* visible */ + if (!s.visible) continue; + var fill = s.textStyle; + if (!fill.opacity || !s.text) continue; + + /* text-baseline, text-align */ + var x = 0, y = 0, dy = 0, anchor = "start"; + switch (s.textBaseline) { + case "middle": dy = ".35em"; break; + case "top": dy = ".71em"; y = s.textMargin; break; + case "bottom": y = "-" + s.textMargin; break; + } + switch (s.textAlign) { + case "right": anchor = "end"; x = "-" + s.textMargin; break; + case "center": anchor = "middle"; break; + case "left": x = s.textMargin; break; + } + + e = this.expect(e, "text", { + "pointer-events": s.events, + "cursor": s.cursor, + "x": x, + "y": y, + "dy": dy, + "transform": "translate(" + s.left + "," + s.top + ")" + + (s.textAngle ? " rotate(" + 180 * s.textAngle / Math.PI + ")" : "") + + (this.scale != 1 ? " scale(" + 1 / this.scale + ")" : ""), + "fill": fill.color, + "fill-opacity": fill.opacity || null, + "text-anchor": anchor + }, { + "font": s.font, + "text-shadow": s.textShadow, + "text-decoration": s.textDecoration + }); + if (e.firstChild) e.firstChild.nodeValue = s.text; + else e.appendChild(document.createTextNode(s.text)); + e = this.append(e, scenes, i); + } + return e; +}; +pv.SvgScene.line = function(scenes) { + var e = scenes.$g.firstChild; + if (scenes.length < 2) return e; + var s = scenes[0]; + + /* segmented */ + if (s.segmented) return this.lineSegment(scenes); + + /* visible */ + if (!s.visible) return e; + var fill = s.fillStyle, stroke = s.strokeStyle; + if (!fill.opacity && !stroke.opacity) return e; + + /* points */ + var d = "M" + s.left + "," + s.top; + + if (scenes.length > 2 && (s.interpolate == "basis" || s.interpolate == "cardinal" || s.interpolate == "monotone")) { + switch (s.interpolate) { + case "basis": d += this.curveBasis(scenes); break; + case "cardinal": d += this.curveCardinal(scenes, s.tension); break; + case "monotone": d += this.curveMonotone(scenes); break; + } + } else { + for (var i = 1; i < scenes.length; i++) { + d += this.pathSegment(scenes[i - 1], scenes[i]); + } + } + + e = this.expect(e, "path", { + "shape-rendering": s.antialias ? null : "crispEdges", + "pointer-events": s.events, + "cursor": s.cursor, + "d": d, + "fill": fill.color, + "fill-opacity": fill.opacity || null, + "stroke": stroke.color, + "stroke-opacity": stroke.opacity || null, + "stroke-width": stroke.opacity ? s.lineWidth / this.scale : null, + "stroke-linejoin": s.lineJoin + }); + return this.append(e, scenes, 0); +}; + +pv.SvgScene.lineSegment = function(scenes) { + var e = scenes.$g.firstChild; + + var s = scenes[0]; + var paths; + switch (s.interpolate) { + case "basis": paths = this.curveBasisSegments(scenes); break; + case "cardinal": paths = this.curveCardinalSegments(scenes, s.tension); break; + case "monotone": paths = this.curveMonotoneSegments(scenes); break; + } + + for (var i = 0, n = scenes.length - 1; i < n; i++) { + var s1 = scenes[i], s2 = scenes[i + 1]; + + /* visible */ + if (!s1.visible || !s2.visible) continue; + var stroke = s1.strokeStyle, fill = pv.Color.transparent; + if (!stroke.opacity) continue; + + /* interpolate */ + var d; + if ((s1.interpolate == "linear") && (s1.lineJoin == "miter")) { + fill = stroke; + stroke = pv.Color.transparent; + d = this.pathJoin(scenes[i - 1], s1, s2, scenes[i + 2]); + } else if(paths) { + d = paths[i]; + } else { + d = "M" + s1.left + "," + s1.top + this.pathSegment(s1, s2); + } + + e = this.expect(e, "path", { + "shape-rendering": s1.antialias ? null : "crispEdges", + "pointer-events": s1.events, + "cursor": s1.cursor, + "d": d, + "fill": fill.color, + "fill-opacity": fill.opacity || null, + "stroke": stroke.color, + "stroke-opacity": stroke.opacity || null, + "stroke-width": stroke.opacity ? s1.lineWidth / this.scale : null, + "stroke-linejoin": s1.lineJoin + }); + e = this.append(e, scenes, i); + } + return e; +}; + +/** @private Returns the path segment for the specified points. */ +pv.SvgScene.pathSegment = function(s1, s2) { + var l = 1; // sweep-flag + switch (s1.interpolate) { + case "polar-reverse": + l = 0; + case "polar": { + var dx = s2.left - s1.left, + dy = s2.top - s1.top, + e = 1 - s1.eccentricity, + r = Math.sqrt(dx * dx + dy * dy) / (2 * e); + if ((e <= 0) || (e > 1)) break; // draw a straight line + return "A" + r + "," + r + " 0 0," + l + " " + s2.left + "," + s2.top; + } + case "step-before": return "V" + s2.top + "H" + s2.left; + case "step-after": return "H" + s2.left + "V" + s2.top; + } + return "L" + s2.left + "," + s2.top; +}; + +/** @private Line-line intersection, per Akenine-Moller 16.16.1. */ +pv.SvgScene.lineIntersect = function(o1, d1, o2, d2) { + return o1.plus(d1.times(o2.minus(o1).dot(d2.perp()) / d1.dot(d2.perp()))); +} + +/** @private Returns the miter join path for the specified points. */ +pv.SvgScene.pathJoin = function(s0, s1, s2, s3) { + /* + * P1-P2 is the current line segment. V is a vector that is perpendicular to + * the line segment, and has length lineWidth / 2. ABCD forms the initial + * bounding box of the line segment (i.e., the line segment if we were to do + * no joins). + */ + var p1 = pv.vector(s1.left, s1.top), + p2 = pv.vector(s2.left, s2.top), + p = p2.minus(p1), + v = p.perp().norm(), + w = v.times(s1.lineWidth / (2 * this.scale)), + a = p1.plus(w), + b = p2.plus(w), + c = p2.minus(w), + d = p1.minus(w); + + /* + * Start join. P0 is the previous line segment's start point. We define the + * cutting plane as the average of the vector perpendicular to P0-P1, and + * the vector perpendicular to P1-P2. This insures that the cross-section of + * the line on the cutting plane is equal if the line-width is unchanged. + * Note that we don't implement miter limits, so these can get wild. + */ + if (s0 && s0.visible) { + var v1 = p1.minus(s0.left, s0.top).perp().norm().plus(v); + d = this.lineIntersect(p1, v1, d, p); + a = this.lineIntersect(p1, v1, a, p); + } + + /* Similarly, for end join. */ + if (s3 && s3.visible) { + var v2 = pv.vector(s3.left, s3.top).minus(p2).perp().norm().plus(v); + c = this.lineIntersect(p2, v2, c, p); + b = this.lineIntersect(p2, v2, b, p); + } + + return "M" + a.x + "," + a.y + + "L" + b.x + "," + b.y + + " " + c.x + "," + c.y + + " " + d.x + "," + d.y; +}; +pv.SvgScene.panel = function(scenes) { + var g = scenes.$g, e = g && g.firstChild; + for (var i = 0; i < scenes.length; i++) { + var s = scenes[i]; + + /* visible */ + if (!s.visible) continue; + + /* svg */ + if (!scenes.parent) { + s.canvas.style.display = "inline-block"; + if (g && (g.parentNode != s.canvas)) { + g = s.canvas.firstChild; + e = g && g.firstChild; + } + if (!g) { + g = s.canvas.appendChild(this.create("svg")); + g.setAttribute("font-size", "10px"); + g.setAttribute("font-family", "sans-serif"); + g.setAttribute("fill", "none"); + g.setAttribute("stroke", "none"); + g.setAttribute("stroke-width", 1.5); + for (var j = 0; j < this.events.length; j++) { + g.addEventListener(this.events[j], this.dispatch, false); + } + e = g.firstChild; + } + scenes.$g = g; + g.setAttribute("width", s.width + s.left + s.right); + g.setAttribute("height", s.height + s.top + s.bottom); + } + + /* clip (nest children) */ + if (s.overflow == "hidden") { + var id = pv.id().toString(36), + c = this.expect(e, "g", {"clip-path": "url(#" + id + ")"}); + if (!c.parentNode) g.appendChild(c); + scenes.$g = g = c; + e = c.firstChild; + + e = this.expect(e, "clipPath", {"id": id}); + var r = e.firstChild || e.appendChild(this.create("rect")); + r.setAttribute("x", s.left); + r.setAttribute("y", s.top); + r.setAttribute("width", s.width); + r.setAttribute("height", s.height); + if (!e.parentNode) g.appendChild(e); + e = e.nextSibling; + } + + /* fill */ + e = this.fill(e, scenes, i); + + /* transform (push) */ + var k = this.scale, + t = s.transform, + x = s.left + t.x, + y = s.top + t.y; + this.scale *= t.k; + + /* children */ + for (var j = 0; j < s.children.length; j++) { + s.children[j].$g = e = this.expect(e, "g", { + "transform": "translate(" + x + "," + y + ")" + + (t.k != 1 ? " scale(" + t.k + ")" : "") + }); + this.updateAll(s.children[j]); + if (!e.parentNode) g.appendChild(e); + e = e.nextSibling; + } + + /* transform (pop) */ + this.scale = k; + + /* stroke */ + e = this.stroke(e, scenes, i); + + /* clip (restore group) */ + if (s.overflow == "hidden") { + scenes.$g = g = c.parentNode; + e = c.nextSibling; + } + } + return e; +}; + +pv.SvgScene.fill = function(e, scenes, i) { + var s = scenes[i], fill = s.fillStyle; + if (fill.opacity || s.events == "all") { + e = this.expect(e, "rect", { + "shape-rendering": s.antialias ? null : "crispEdges", + "pointer-events": s.events, + "cursor": s.cursor, + "x": s.left, + "y": s.top, + "width": s.width, + "height": s.height, + "fill": fill.color, + "fill-opacity": fill.opacity, + "stroke": null + }); + e = this.append(e, scenes, i); + } + return e; +}; + +pv.SvgScene.stroke = function(e, scenes, i) { + var s = scenes[i], stroke = s.strokeStyle; + if (stroke.opacity || s.events == "all") { + e = this.expect(e, "rect", { + "shape-rendering": s.antialias ? null : "crispEdges", + "pointer-events": s.events == "all" ? "stroke" : s.events, + "cursor": s.cursor, + "x": s.left, + "y": s.top, + "width": Math.max(1E-10, s.width), + "height": Math.max(1E-10, s.height), + "fill": null, + "stroke": stroke.color, + "stroke-opacity": stroke.opacity, + "stroke-width": s.lineWidth / this.scale + }); + e = this.append(e, scenes, i); + } + return e; +}; +pv.SvgScene.rule = function(scenes) { + var e = scenes.$g.firstChild; + for (var i = 0; i < scenes.length; i++) { + var s = scenes[i]; + + /* visible */ + if (!s.visible) continue; + var stroke = s.strokeStyle; + if (!stroke.opacity) continue; + + e = this.expect(e, "line", { + "shape-rendering": s.antialias ? null : "crispEdges", + "pointer-events": s.events, + "cursor": s.cursor, + "x1": s.left, + "y1": s.top, + "x2": s.left + s.width, + "y2": s.top + s.height, + "stroke": stroke.color, + "stroke-opacity": stroke.opacity, + "stroke-width": s.lineWidth / this.scale + }); + e = this.append(e, scenes, i); + } + return e; +}; +pv.SvgScene.wedge = function(scenes) { + var e = scenes.$g.firstChild; + for (var i = 0; i < scenes.length; i++) { + var s = scenes[i]; + + /* visible */ + if (!s.visible) continue; + var fill = s.fillStyle, stroke = s.strokeStyle; + if (!fill.opacity && !stroke.opacity) continue; + + /* points */ + var r1 = s.innerRadius, r2 = s.outerRadius, a = Math.abs(s.angle), p; + if (a >= 2 * Math.PI) { + if (r1) { + p = "M0," + r2 + + "A" + r2 + "," + r2 + " 0 1,1 0," + (-r2) + + "A" + r2 + "," + r2 + " 0 1,1 0," + r2 + + "M0," + r1 + + "A" + r1 + "," + r1 + " 0 1,1 0," + (-r1) + + "A" + r1 + "," + r1 + " 0 1,1 0," + r1 + + "Z"; + } else { + p = "M0," + r2 + + "A" + r2 + "," + r2 + " 0 1,1 0," + (-r2) + + "A" + r2 + "," + r2 + " 0 1,1 0," + r2 + + "Z"; + } + } else { + var sa = Math.min(s.startAngle, s.endAngle), + ea = Math.max(s.startAngle, s.endAngle), + c1 = Math.cos(sa), c2 = Math.cos(ea), + s1 = Math.sin(sa), s2 = Math.sin(ea); + if (r1) { + p = "M" + r2 * c1 + "," + r2 * s1 + + "A" + r2 + "," + r2 + " 0 " + + ((a < Math.PI) ? "0" : "1") + ",1 " + + r2 * c2 + "," + r2 * s2 + + "L" + r1 * c2 + "," + r1 * s2 + + "A" + r1 + "," + r1 + " 0 " + + ((a < Math.PI) ? "0" : "1") + ",0 " + + r1 * c1 + "," + r1 * s1 + "Z"; + } else { + p = "M" + r2 * c1 + "," + r2 * s1 + + "A" + r2 + "," + r2 + " 0 " + + ((a < Math.PI) ? "0" : "1") + ",1 " + + r2 * c2 + "," + r2 * s2 + "L0,0Z"; + } + } + + e = this.expect(e, "path", { + "shape-rendering": s.antialias ? null : "crispEdges", + "pointer-events": s.events, + "cursor": s.cursor, + "transform": "translate(" + s.left + "," + s.top + ")", + "d": p, + "fill": fill.color, + "fill-rule": "evenodd", + "fill-opacity": fill.opacity || null, + "stroke": stroke.color, + "stroke-opacity": stroke.opacity || null, + "stroke-width": stroke.opacity ? s.lineWidth / this.scale : null + }); + e = this.append(e, scenes, i); + } + return e; +}; +/** + * Constructs a new mark with default properties. Marks, with the exception of + * the root panel, are not typically constructed directly; instead, they are + * added to a panel or an existing mark via {@link pv.Mark#add}. + * + * @class Represents a data-driven graphical mark. The <tt>Mark</tt> class is + * the base class for all graphical marks in Protovis; it does not provide any + * specific rendering functionality, but together with {@link Panel} establishes + * the core framework. + * + * <p>Concrete mark types include familiar visual elements such as bars, lines + * and labels. Although a bar mark may be used to construct a bar chart, marks + * know nothing about charts; it is only through their specification and + * composition that charts are produced. These building blocks permit many + * combinatorial possibilities. + * + * <p>Marks are associated with <b>data</b>: a mark is generated once per + * associated datum, mapping the datum to visual <b>properties</b> such as + * position and color. Thus, a single mark specification represents a set of + * visual elements that share the same data and visual encoding. The type of + * mark defines the names of properties and their meaning. A property may be + * static, ignoring the associated datum and returning a constant; or, it may be + * dynamic, derived from the associated datum or index. Such dynamic encodings + * can be specified succinctly using anonymous functions. Special properties + * called event handlers can be registered to add interactivity. + * + * <p>Protovis uses <b>inheritance</b> to simplify the specification of related + * marks: a new mark can be derived from an existing mark, inheriting its + * properties. The new mark can then override properties to specify new + * behavior, potentially in terms of the old behavior. In this way, the old mark + * serves as the <b>prototype</b> for the new mark. Most mark types share the + * same basic properties for consistency and to facilitate inheritance. + * + * <p>The prioritization of redundant properties is as follows:<ol> + * + * <li>If the <tt>width</tt> property is not specified (i.e., null), its value + * is the width of the parent panel, minus this mark's left and right margins; + * the left and right margins are zero if not specified. + * + * <li>Otherwise, if the <tt>right</tt> margin is not specified, its value is + * the width of the parent panel, minus this mark's width and left margin; the + * left margin is zero if not specified. + * + * <li>Otherwise, if the <tt>left</tt> property is not specified, its value is + * the width of the parent panel, minus this mark's width and the right margin. + * + * </ol>This prioritization is then duplicated for the <tt>height</tt>, + * <tt>bottom</tt> and <tt>top</tt> properties, respectively. + * + * <p>While most properties are <i>variable</i>, some mark types, such as lines + * and areas, generate a single visual element rather than a distinct visual + * element per datum. With these marks, some properties may be <b>fixed</b>. + * Fixed properties can vary per mark, but not <i>per datum</i>! These + * properties are evaluated solely for the first (0-index) datum, and typically + * are specified as a constant. However, it is valid to use a function if the + * property varies between panels or is dynamically generated. + * + * <p>See also the <a href="../../api/">Protovis guide</a>. + */ +pv.Mark = function() { + /* + * TYPE 0 constant defs + * TYPE 1 function defs + * TYPE 2 constant properties + * TYPE 3 function properties + * in order of evaluation! + */ + this.$properties = []; + this.$handlers = {}; +}; + +/** @private Records which properties are defined on this mark type. */ +pv.Mark.prototype.properties = {}; + +/** @private Records the cast function for each property. */ +pv.Mark.cast = {}; + +/** + * @private Defines and registers a property method for the property with the + * given name. This method should be called on a mark class prototype to define + * each exposed property. (Note this refers to the JavaScript + * <tt>prototype</tt>, not the Protovis mark prototype, which is the {@link + * #proto} field.) + * + * <p>The created property method supports several modes of invocation: <ol> + * + * <li>If invoked with a <tt>Function</tt> argument, this function is evaluated + * for each associated datum. The return value of the function is used as the + * computed property value. The context of the function (<tt>this</tt>) is this + * mark. The arguments to the function are the associated data of this mark and + * any enclosing panels. For example, a linear encoding of numerical data to + * height is specified as + * + * <pre>m.height(function(d) d * 100);</pre> + * + * The expression <tt>d * 100</tt> will be evaluated for the height property of + * each mark instance. The return value of the property method (e.g., + * <tt>m.height</tt>) is this mark (<tt>m</tt>)).<p> + * + * <li>If invoked with a non-function argument, the property is treated as a + * constant. The return value of the property method (e.g., <tt>m.height</tt>) + * is this mark.<p> + * + * <li>If invoked with no arguments, the computed property value for the current + * mark instance in the scene graph is returned. This facilitates <i>property + * chaining</i>, where one mark's properties are defined in terms of another's. + * For example, to offset a mark's location from its prototype, you might say + * + * <pre>m.top(function() this.proto.top() + 10);</pre> + * + * Note that the index of the mark being evaluated (in the above example, + * <tt>this.proto</tt>) is inherited from the <tt>Mark</tt> class and set by + * this mark. So, if the fifth element's top property is being evaluated, the + * fifth instance of <tt>this.proto</tt> will similarly be queried for the value + * of its top property. If the mark being evaluated has a different number of + * instances, or its data is unrelated, the behavior of this method is + * undefined. In these cases it may be better to index the <tt>scene</tt> + * explicitly to specify the exact instance. + * + * </ol><p>Property names should follow standard JavaScript method naming + * conventions, using lowerCamel-style capitalization. + * + * <p>In addition to creating the property method, every property is registered + * in the {@link #properties} map on the <tt>prototype</tt>. Although this is an + * instance field, it is considered immutable and shared by all instances of a + * given mark type. The <tt>properties</tt> map can be queried to see if a mark + * type defines a particular property, such as width or height. + * + * @param {string} name the property name. + * @param {function} [cast] the cast function for this property. + */ +pv.Mark.prototype.property = function(name, cast) { + if (!this.hasOwnProperty("properties")) { + this.properties = pv.extend(this.properties); + } + this.properties[name] = true; + + /* + * Define the setter-getter globally, since the default behavior should be the + * same for all properties, and since the Protovis inheritance chain is + * independent of the JavaScript inheritance chain. For example, anchors + * define a "name" property that is evaluated on derived marks, even though + * those marks don't normally have a name. + */ + pv.Mark.prototype.propertyMethod(name, false, pv.Mark.cast[name] = cast); + return this; +}; + +/** + * @private Defines a setter-getter for the specified property. + * + * <p>If a cast function has been assigned to the specified property name, the + * property function is wrapped by the cast function, or, if a constant is + * specified, the constant is immediately cast. Note, however, that if the + * property value is null, the cast function is not invoked. + * + * @param {string} name the property name. + * @param {boolean} [def] whether is a property or a def. + * @param {function} [cast] the cast function for this property. + */ +pv.Mark.prototype.propertyMethod = function(name, def, cast) { + if (!cast) cast = pv.Mark.cast[name]; + this[name] = function(v) { + + /* If this is a def, use it rather than property. */ + if (def && this.scene) { + var defs = this.scene.defs; + if (arguments.length) { + defs[name] = { + id: (v == null) ? 0 : pv.id(), + value: ((v != null) && cast) ? cast(v) : v + }; + return this; + } + return defs[name] ? defs[name].value : null; + } + + /* If arguments are specified, set the property value. */ + if (arguments.length) { + var type = !def << 1 | (typeof v == "function"); + this.propertyValue(name, (type & 1 && cast) ? function() { + var x = v.apply(this, arguments); + return (x != null) ? cast(x) : null; + } : (((v != null) && cast) ? cast(v) : v)).type = type; + return this; + } + + return this.instance()[name]; + }; +}; + +/** @private Sets the value of the property <i>name</i> to <i>v</i>. */ +pv.Mark.prototype.propertyValue = function(name, v) { + var properties = this.$properties, p = {name: name, id: pv.id(), value: v}; + for (var i = 0; i < properties.length; i++) { + if (properties[i].name == name) { + properties.splice(i, 1); + break; + } + } + properties.push(p); + return p; +}; + +/* Define all global properties. */ +pv.Mark.prototype + .property("data") + .property("visible", Boolean) + .property("left", Number) + .property("right", Number) + .property("top", Number) + .property("bottom", Number) + .property("cursor", String) + .property("title", String) + .property("reverse", Boolean) + .property("antialias", Boolean) + .property("events", String); + +/** + * The mark type; a lower camelCase name. The type name controls rendering + * behavior, and unless the rendering engine is extended, must be one of the + * built-in concrete mark types: area, bar, dot, image, label, line, panel, + * rule, or wedge. + * + * @type string + * @name pv.Mark.prototype.type + */ + +/** + * The mark prototype, possibly undefined, from which to inherit property + * functions. The mark prototype is not necessarily of the same type as this + * mark. Any properties defined on this mark will override properties inherited + * either from the prototype or from the type-specific defaults. + * + * @type pv.Mark + * @name pv.Mark.prototype.proto + */ + +/** + * The mark anchor target, possibly undefined. + * + * @type pv.Mark + * @name pv.Mark.prototype.target + */ + +/** + * The enclosing parent panel. The parent panel is generally undefined only for + * the root panel; however, it is possible to create "offscreen" marks that are + * used only for inheritance purposes. + * + * @type pv.Panel + * @name pv.Mark.prototype.parent + */ + +/** + * The child index. -1 if the enclosing parent panel is null; otherwise, the + * zero-based index of this mark into the parent panel's <tt>children</tt> array. + * + * @type number + */ +pv.Mark.prototype.childIndex = -1; + +/** + * The mark index. The value of this field depends on which instance (i.e., + * which element of the data array) is currently being evaluated. During the + * build phase, the index is incremented over each datum; when handling events, + * the index is set to the instance that triggered the event. + * + * @type number + */ +pv.Mark.prototype.index = -1; + +/** + * The current scale factor, based on any enclosing transforms. The current + * scale can be used to create scale-independent graphics. For example, to + * define a dot that has a radius of 10 irrespective of any zooming, say: + * + * <pre>dot.radius(function() 10 / this.scale)</pre> + * + * Note that the stroke width and font size are defined irrespective of scale + * (i.e., in screen space) already. Also note that when a transform is applied + * to a panel, the scale affects only the child marks, not the panel itself. + * + * @type number + * @see pv.Panel#transform + */ +pv.Mark.prototype.scale = 1; + +/** + * @private The scene graph. The scene graph is an array of objects; each object + * (or "node") corresponds to an instance of this mark and an element in the + * data array. The scene graph can be traversed to lookup previously-evaluated + * properties. + * + * @name pv.Mark.prototype.scene + */ + +/** + * The root parent panel. This may be undefined for "offscreen" marks that are + * created for inheritance purposes only. + * + * @type pv.Panel + * @name pv.Mark.prototype.root + */ + +/** + * The data property; an array of objects. The size of the array determines the + * number of marks that will be instantiated; each element in the array will be + * passed to property functions to compute the property values. Typically, the + * data property is specified as a constant array, such as + * + * <pre>m.data([1, 2, 3, 4, 5]);</pre> + * + * However, it is perfectly acceptable to define the data property as a + * function. This function might compute the data dynamically, allowing + * different data to be used per enclosing panel. For instance, in the stacked + * area graph example (see {@link #scene}), the data function on the area mark + * dereferences each series. + * + * @type array + * @name pv.Mark.prototype.data + */ + +/** + * The visible property; a boolean determining whether or not the mark instance + * is visible. If a mark instance is not visible, its other properties will not + * be evaluated. Similarly, for panels no child marks will be rendered. + * + * @type boolean + * @name pv.Mark.prototype.visible + */ + +/** + * The left margin; the distance, in pixels, between the left edge of the + * enclosing panel and the left edge of this mark. Note that in some cases this + * property may be redundant with the right property, or with the conjunction of + * right and width. + * + * @type number + * @name pv.Mark.prototype.left + */ + +/** + * The right margin; the distance, in pixels, between the right edge of the + * enclosing panel and the right edge of this mark. Note that in some cases this + * property may be redundant with the left property, or with the conjunction of + * left and width. + * + * @type number + * @name pv.Mark.prototype.right + */ + +/** + * The top margin; the distance, in pixels, between the top edge of the + * enclosing panel and the top edge of this mark. Note that in some cases this + * property may be redundant with the bottom property, or with the conjunction + * of bottom and height. + * + * @type number + * @name pv.Mark.prototype.top + */ + +/** + * The bottom margin; the distance, in pixels, between the bottom edge of the + * enclosing panel and the bottom edge of this mark. Note that in some cases + * this property may be redundant with the top property, or with the conjunction + * of top and height. + * + * @type number + * @name pv.Mark.prototype.bottom + */ + +/** + * The cursor property; corresponds to the CSS cursor property. This is + * typically used in conjunction with event handlers to indicate interactivity. + * + * @type string + * @name pv.Mark.prototype.cursor + * @see <a href="http://www.w3.org/TR/CSS2/ui.html#propdef-cursor">CSS2 cursor</a> + */ + +/** + * The title property; corresponds to the HTML/SVG title property, allowing the + * general of simple plain text tooltips. + * + * @type string + * @name pv.Mark.prototype.title + */ + +/** + * The events property; corresponds to the SVG pointer-events property, + * specifying how the mark should participate in mouse events. The default value + * is "painted". Supported values are: + * + * <p>"painted": The given mark may receive events when the mouse is over a + * "painted" area. The painted areas are the interior (i.e., fill) of the mark + * if a 'fillStyle' is specified, and the perimeter (i.e., stroke) of the mark + * if a 'strokeStyle' is specified. + * + * <p>"all": The given mark may receive events when the mouse is over either the + * interior (i.e., fill) or the perimeter (i.e., stroke) of the mark, regardless + * of the specified fillStyle and strokeStyle. + * + * <p>"none": The given mark may not receive events. + * + * @type string + * @name pv.Mark.prototype.events + */ + +/** + * The reverse property; a boolean determining whether marks are ordered from + * front-to-back or back-to-front. SVG does not support explicit z-ordering; + * shapes are rendered in the order they appear. Thus, by default, marks are + * rendered in data order. Setting the reverse property to false reverses the + * order in which they are rendered; however, the properties are still evaluated + * (i.e., built) in forward order. + * + * @type boolean + * @name pv.Mark.prototype.reverse + */ + +/** + * Default properties for all mark types. By default, the data array is the + * parent data as a single-element array; if the data property is not specified, + * this causes each mark to be instantiated as a singleton with the parents + * datum. The visible property is true by default, and the reverse property is + * false. + * + * @type pv.Mark + */ +pv.Mark.prototype.defaults = new pv.Mark() + .data(function(d) { return [d]; }) + .visible(true) + .antialias(true) + .events("painted"); + +/** + * Sets the prototype of this mark to the specified mark. Any properties not + * defined on this mark may be inherited from the specified prototype mark, or + * its prototype, and so on. The prototype mark need not be the same type of + * mark as this mark. (Note that for inheritance to be useful, properties with + * the same name on different mark types should have equivalent meaning.) + * + * @param {pv.Mark} proto the new prototype. + * @returns {pv.Mark} this mark. + * @see #add + */ +pv.Mark.prototype.extend = function(proto) { + this.proto = proto; + this.target = proto.target; + return this; +}; + +/** + * Adds a new mark of the specified type to the enclosing parent panel, whilst + * simultaneously setting the prototype of the new mark to be this mark. + * + * @param {function} type the type of mark to add; a constructor, such as + * <tt>pv.Bar</tt>. + * @returns {pv.Mark} the new mark. + * @see #extend + */ +pv.Mark.prototype.add = function(type) { + return this.parent.add(type).extend(this); +}; + +/** + * Defines a custom property on this mark. Custom properties are currently + * fixed, in that they are initialized once per mark set (i.e., per parent panel + * instance). Custom properties can be used to store local state for the mark, + * such as data needed by other properties (e.g., a custom scale) or interaction + * state. + * + * <p>WARNING We plan on changing this feature in a future release to define + * standard properties, as opposed to <i>fixed</i> properties that behave + * idiosyncratically within event handlers. Furthermore, we recommend storing + * state in an external data structure, rather than tying it to the + * visualization specification as with defs. + * + * @param {string} name the name of the local variable. + * @param {function} [v] an optional initializer; may be a constant or a + * function. + */ +pv.Mark.prototype.def = function(name, v) { + this.propertyMethod(name, true); + return this[name](arguments.length > 1 ? v : null); +}; + +/** + * Returns an anchor with the specified name. All marks support the five + * standard anchor names:<ul> + * + * <li>top + * <li>left + * <li>center + * <li>bottom + * <li>right + * + * </ul>In addition to positioning properties (left, right, top bottom), the + * anchors support text rendering properties (text-align, text-baseline). Text is + * rendered to appear inside the mark by default. + * + * <p>To facilitate stacking, anchors are defined in terms of their opposite + * edge. For example, the top anchor defines the bottom property, such that the + * mark extends upwards; the bottom anchor instead defines the top property, + * such that the mark extends downwards. See also {@link pv.Layout.Stack}. + * + * <p>While anchor names are typically constants, the anchor name is a true + * property, which means you can specify a function to compute the anchor name + * dynamically. See the {@link pv.Anchor#name} property for details. + * + * @param {string} name the anchor name; either a string or a property function. + * @returns {pv.Anchor} the new anchor. + */ +pv.Mark.prototype.anchor = function(name) { + if (!name) name = "center"; // default anchor name + return new pv.Anchor(this) + .name(name) + .data(function() { + return this.scene.target.map(function(s) { return s.data; }); + }) + .visible(function() { + return this.scene.target[this.index].visible; + }) + .left(function() { + var s = this.scene.target[this.index], w = s.width || 0; + switch (this.name()) { + case "bottom": + case "top": + case "center": return s.left + w / 2; + case "left": return null; + } + return s.left + w; + }) + .top(function() { + var s = this.scene.target[this.index], h = s.height || 0; + switch (this.name()) { + case "left": + case "right": + case "center": return s.top + h / 2; + case "top": return null; + } + return s.top + h; + }) + .right(function() { + var s = this.scene.target[this.index]; + return this.name() == "left" ? s.right + (s.width || 0) : null; + }) + .bottom(function() { + var s = this.scene.target[this.index]; + return this.name() == "top" ? s.bottom + (s.height || 0) : null; + }) + .textAlign(function() { + switch (this.name()) { + case "bottom": + case "top": + case "center": return "center"; + case "right": return "right"; + } + return "left"; + }) + .textBaseline(function() { + switch (this.name()) { + case "right": + case "left": + case "center": return "middle"; + case "top": return "top"; + } + return "bottom"; + }); +}; + +/** @deprecated Replaced by {@link #target}. */ +pv.Mark.prototype.anchorTarget = function() { + return this.target; +}; + +/** + * Alias for setting the left, right, top and bottom properties simultaneously. + * + * @see #left + * @see #right + * @see #top + * @see #bottom + * @returns {pv.Mark} this. + */ +pv.Mark.prototype.margin = function(n) { + return this.left(n).right(n).top(n).bottom(n); +}; + +/** + * @private Returns the current instance of this mark in the scene graph. This + * is typically equivalent to <tt>this.scene[this.index]</tt>, however if the + * scene or index is unset, the default instance of the mark is returned. If no + * default is set, the default is the last instance. Similarly, if the scene or + * index of the parent panel is unset, the default instance of this mark in the + * last instance of the enclosing panel is returned, and so on. + * + * @returns a node in the scene graph. + */ +pv.Mark.prototype.instance = function(defaultIndex) { + var scene = this.scene || this.parent.instance(-1).children[this.childIndex], + index = !arguments.length || this.hasOwnProperty("index") ? this.index : defaultIndex; + return scene[index < 0 ? scene.length - 1 : index]; +}; + +/** + * @private Find the instances of this mark that match source. + * + * @see pv.Anchor + */ +pv.Mark.prototype.instances = function(source) { + var mark = this, index = [], scene; + + /* Mirrored descent. */ + while (!(scene = mark.scene)) { + source = source.parent; + index.push({index: source.index, childIndex: mark.childIndex}); + mark = mark.parent; + } + while (index.length) { + var i = index.pop(); + scene = scene[i.index].children[i.childIndex]; + } + + /* + * When the anchor target is also an ancestor, as in the case of adding + * to a panel anchor, only generate one instance per panel. Also, set + * the margins to zero, since they are offset by the enclosing panel. + */ + if (this.hasOwnProperty("index")) { + var s = pv.extend(scene[this.index]); + s.right = s.top = s.left = s.bottom = 0; + return [s]; + } + return scene; +}; + +/** + * @private Returns the first instance of this mark in the scene graph. This + * method can only be called when the mark is bound to the scene graph (for + * example, from an event handler, or within a property function). + * + * @returns a node in the scene graph. + */ +pv.Mark.prototype.first = function() { + return this.scene[0]; +}; + +/** + * @private Returns the last instance of this mark in the scene graph. This + * method can only be called when the mark is bound to the scene graph (for + * example, from an event handler, or within a property function). In addition, + * note that mark instances are built sequentially, so the last instance of this + * mark may not yet be constructed. + * + * @returns a node in the scene graph. + */ +pv.Mark.prototype.last = function() { + return this.scene[this.scene.length - 1]; +}; + +/** + * @private Returns the previous instance of this mark in the scene graph, or + * null if this is the first instance. + * + * @returns a node in the scene graph, or null. + */ +pv.Mark.prototype.sibling = function() { + return (this.index == 0) ? null : this.scene[this.index - 1]; +}; + +/** + * @private Returns the current instance in the scene graph of this mark, in the + * previous instance of the enclosing parent panel. May return null if this + * instance could not be found. + * + * @returns a node in the scene graph, or null. + */ +pv.Mark.prototype.cousin = function() { + var p = this.parent, s = p && p.sibling(); + return (s && s.children) ? s.children[this.childIndex][this.index] : null; +}; + +/** + * Renders this mark, including recursively rendering all child marks if this is + * a panel. This method finds all instances of this mark and renders them. This + * method descends recursively to the level of the mark to be rendered, finding + * all visible instances of the mark. After the marks are rendered, the scene + * and index attributes are removed from the mark to restore them to a clean + * state. + * + * <p>If an enclosing panel has an index property set (as is the case inside in + * an event handler), then only instances of this mark inside the given instance + * of the panel will be rendered; otherwise, all visible instances of the mark + * will be rendered. + */ +pv.Mark.prototype.render = function() { + var parent = this.parent, + stack = pv.Mark.stack; + + /* For the first render, take it from the top. */ + if (parent && !this.root.scene) { + this.root.render(); + return; + } + + /* Record the path to this mark. */ + var indexes = []; + for (var mark = this; mark.parent; mark = mark.parent) { + indexes.unshift(mark.childIndex); + } + + /** @private */ + function render(mark, depth, scale) { + mark.scale = scale; + if (depth < indexes.length) { + stack.unshift(null); + if (mark.hasOwnProperty("index")) { + renderInstance(mark, depth, scale); + } else { + for (var i = 0, n = mark.scene.length; i < n; i++) { + mark.index = i; + renderInstance(mark, depth, scale); + } + delete mark.index; + } + stack.shift(); + } else { + mark.build(); + + /* + * In the update phase, the scene is rendered by creating and updating + * elements and attributes in the SVG image. No properties are evaluated + * during the update phase; instead the values computed previously in the + * build phase are simply translated into SVG. The update phase is + * decoupled (see pv.Scene) to allow different rendering engines. + */ + pv.Scene.scale = scale; + pv.Scene.updateAll(mark.scene); + } + delete mark.scale; + } + + /** + * @private Recursively renders the current instance of the specified mark. + * This is slightly tricky because `index` and `scene` properties may or may + * not already be set; if they are set, it means we are rendering only a + * specific instance; if they are unset, we are rendering all instances. + * Furthermore, we must preserve the original context of these properties when + * rendering completes. + * + * <p>Another tricky aspect is that the `scene` attribute should be set for + * any preceding children, so as to allow property chaining. This is + * consistent with first-pass rendering. + */ + function renderInstance(mark, depth, scale) { + var s = mark.scene[mark.index], i; + if (s.visible) { + var childIndex = indexes[depth], + child = mark.children[childIndex]; + + /* Set preceding child scenes. */ + for (i = 0; i < childIndex; i++) { + mark.children[i].scene = s.children[i]; + } + + /* Set current child scene, if necessary. */ + stack[0] = s.data; + if (child.scene) { + render(child, depth + 1, scale * s.transform.k); + } else { + child.scene = s.children[childIndex]; + render(child, depth + 1, scale * s.transform.k); + delete child.scene; + } + + /* Clear preceding child scenes. */ + for (i = 0; i < childIndex; i++) { + delete mark.children[i].scene; + } + } + } + + /* Bind this mark's property definitions. */ + this.bind(); + + /* The render context is the first ancestor with an explicit index. */ + while (parent && !parent.hasOwnProperty("index")) parent = parent.parent; + + /* Recursively render all instances of this mark. */ + this.context( + parent ? parent.scene : undefined, + parent ? parent.index : -1, + function() { render(this.root, 0, 1); }); +}; + +/** @private Stores the current data stack. */ +pv.Mark.stack = []; + +/** + * @private In the bind phase, inherited property definitions are cached so they + * do not need to be queried during build. + */ +pv.Mark.prototype.bind = function() { + var seen = {}, types = [[], [], [], []], data, visible; + + /** Scans the proto chain for the specified mark. */ + function bind(mark) { + do { + var properties = mark.$properties; + for (var i = properties.length - 1; i >= 0 ; i--) { + var p = properties[i]; + if (!(p.name in seen)) { + seen[p.name] = p; + switch (p.name) { + case "data": data = p; break; + case "visible": visible = p; break; + default: types[p.type].push(p); break; + } + } + } + } while (mark = mark.proto); + } + + /* Scan the proto chain for all defined properties. */ + bind(this); + bind(this.defaults); + types[1].reverse(); + types[3].reverse(); + + /* Any undefined properties are null. */ + var mark = this; + do for (var name in mark.properties) { + if (!(name in seen)) { + types[2].push(seen[name] = {name: name, type: 2, value: null}); + } + } while (mark = mark.proto); + + /* Define setter-getter for inherited defs. */ + var defs = types[0].concat(types[1]); + for (var i = 0; i < defs.length; i++) { + this.propertyMethod(defs[i].name, true); + } + + /* Setup binds to evaluate constants before functions. */ + this.binds = { + properties: seen, + data: data, + defs: defs, + required: [visible], + optional: pv.blend(types) + }; +}; + +/** + * @private Evaluates properties and computes implied properties. Properties are + * stored in the {@link #scene} array for each instance of this mark. + * + * <p>As marks are built recursively, the {@link #index} property is updated to + * match the current index into the data array for each mark. Note that the + * index property is only set for the mark currently being built and its + * enclosing parent panels. The index property for other marks is unset, but is + * inherited from the global <tt>Mark</tt> class prototype. This allows mark + * properties to refer to properties on other marks <i>in the same panel</i> + * conveniently; however, in general it is better to reference mark instances + * specifically through the scene graph rather than depending on the magical + * behavior of {@link #index}. + * + * <p>The root scene array has a special property, <tt>data</tt>, which stores + * the current data stack. The first element in this stack is the current datum, + * followed by the datum of the enclosing parent panel, and so on. The data + * stack should not be accessed directly; instead, property functions are passed + * the current data stack as arguments. + * + * <p>The evaluation of the <tt>data</tt> and <tt>visible</tt> properties is + * special. The <tt>data</tt> property is evaluated first; unlike the other + * properties, the data stack is from the parent panel, rather than the current + * mark, since the data is not defined until the data property is evaluated. + * The <tt>visisble</tt> property is subsequently evaluated for each instance; + * only if true will the {@link #buildInstance} method be called, evaluating + * other properties and recursively building the scene graph. + * + * <p>If this mark is being re-built, any old instances of this mark that no + * longer exist (because the new data array contains fewer elements) will be + * cleared using {@link #clearInstance}. + * + * @param parent the instance of the parent panel from the scene graph. + */ +pv.Mark.prototype.build = function() { + var scene = this.scene, stack = pv.Mark.stack; + if (!scene) { + scene = this.scene = []; + scene.mark = this; + scene.type = this.type; + scene.childIndex = this.childIndex; + if (this.parent) { + scene.parent = this.parent.scene; + scene.parentIndex = this.parent.index; + } + } + + /* Resolve anchor target. */ + if (this.target) scene.target = this.target.instances(scene); + + /* Evaluate defs. */ + if (this.binds.defs.length) { + var defs = scene.defs; + if (!defs) scene.defs = defs = {}; + for (var i = 0; i < this.binds.defs.length; i++) { + var p = this.binds.defs[i], d = defs[p.name]; + if (!d || (p.id > d.id)) { + defs[p.name] = { + id: 0, // this def will be re-evaluated on next build + value: (p.type & 1) ? p.value.apply(this, stack) : p.value + }; + } + } + } + + /* Evaluate special data property. */ + var data = this.binds.data; + data = data.type & 1 ? data.value.apply(this, stack) : data.value; + + /* Create, update and delete scene nodes. */ + stack.unshift(null); + scene.length = data.length; + for (var i = 0; i < data.length; i++) { + pv.Mark.prototype.index = this.index = i; + var s = scene[i]; + if (!s) scene[i] = s = {}; + s.data = stack[0] = data[i]; + this.buildInstance(s); + } + pv.Mark.prototype.index = -1; + delete this.index; + stack.shift(); + + return this; +}; + +/** + * @private Evaluates the specified array of properties for the specified + * instance <tt>s</tt> in the scene graph. + * + * @param s a node in the scene graph; the instance of the mark to build. + * @param properties an array of properties. + */ +pv.Mark.prototype.buildProperties = function(s, properties) { + for (var i = 0, n = properties.length; i < n; i++) { + var p = properties[i], v = p.value; // assume case 2 (constant) + switch (p.type) { + case 0: + case 1: v = this.scene.defs[p.name].value; break; + case 3: v = v.apply(this, pv.Mark.stack); break; + } + s[p.name] = v; + } +}; + +/** + * @private Evaluates all of the properties for this mark for the specified + * instance <tt>s</tt> in the scene graph. The set of properties to evaluate is + * retrieved from the {@link #properties} array for this mark type (see {@link + * #type}). After these properties are evaluated, any <b>implied</b> properties + * may be computed by the mark and set on the scene graph; see + * {@link #buildImplied}. + * + * <p>For panels, this method recursively builds the scene graph for all child + * marks as well. In general, this method should not need to be overridden by + * concrete mark types. + * + * @param s a node in the scene graph; the instance of the mark to build. + */ +pv.Mark.prototype.buildInstance = function(s) { + this.buildProperties(s, this.binds.required); + if (s.visible) { + this.buildProperties(s, this.binds.optional); + this.buildImplied(s); + } +}; + +/** + * @private Computes the implied properties for this mark for the specified + * instance <tt>s</tt> in the scene graph. Implied properties are those with + * dependencies on multiple other properties; for example, the width property + * may be implied if the left and right properties are set. This method can be + * overridden by concrete mark types to define new implied properties, if + * necessary. + * + * @param s a node in the scene graph; the instance of the mark to build. + */ +pv.Mark.prototype.buildImplied = function(s) { + var l = s.left; + var r = s.right; + var t = s.top; + var b = s.bottom; + + /* Assume width and height are zero if not supported by this mark type. */ + var p = this.properties; + var w = p.width ? s.width : 0; + var h = p.height ? s.height : 0; + + /* Compute implied width, right and left. */ + var width = this.parent ? this.parent.width() : (w + l + r); + if (w == null) { + w = width - (r = r || 0) - (l = l || 0); + } else if (r == null) { + if (l == null) { + l = r = (width - w) / 2; + } else { + r = width - w - (l = l || 0); + } + } else if (l == null) { + l = width - w - r; + } + + /* Compute implied height, bottom and top. */ + var height = this.parent ? this.parent.height() : (h + t + b); + if (h == null) { + h = height - (t = t || 0) - (b = b || 0); + } else if (b == null) { + if (t == null) { + b = t = (height - h) / 2; + } else { + b = height - h - (t = t || 0); + } + } else if (t == null) { + t = height - h - b; + } + + s.left = l; + s.right = r; + s.top = t; + s.bottom = b; + + /* Only set width and height if they are supported by this mark type. */ + if (p.width) s.width = w; + if (p.height) s.height = h; + + /* Set any null colors to pv.Color.transparent. */ + if (p.textStyle && !s.textStyle) s.textStyle = pv.Color.transparent; + if (p.fillStyle && !s.fillStyle) s.fillStyle = pv.Color.transparent; + if (p.strokeStyle && !s.strokeStyle) s.strokeStyle = pv.Color.transparent; +}; + +/** + * Returns the current location of the mouse (cursor) relative to this mark's + * parent. The <i>x</i> coordinate corresponds to the left margin, while the + * <i>y</i> coordinate corresponds to the top margin. + * + * @returns {pv.Vector} the mouse location. + */ +pv.Mark.prototype.mouse = function() { + + /* Compute xy-coordinates relative to the panel. */ + var x = pv.event.pageX || 0, + y = pv.event.pageY || 0, + n = this.root.canvas(); + do { + x -= n.offsetLeft; + y -= n.offsetTop; + } while (n = n.offsetParent); + + /* Compute the inverse transform of all enclosing panels. */ + var t = pv.Transform.identity, + p = this.properties.transform ? this : this.parent, + pz = []; + do { pz.push(p); } while (p = p.parent); + while (p = pz.pop()) t = t.translate(p.left(), p.top()).times(p.transform()); + t = t.invert(); + + return pv.vector(x * t.k + t.x, y * t.k + t.y); +}; + +/** + * Registers an event handler for the specified event type with this mark. When + * an event of the specified type is triggered, the specified handler will be + * invoked. The handler is invoked in a similar method to property functions: + * the context is <tt>this</tt> mark instance, and the arguments are the full + * data stack. Event handlers can use property methods to manipulate the display + * properties of the mark: + * + * <pre>m.event("click", function() this.fillStyle("red"));</pre> + * + * Alternatively, the external data can be manipulated and the visualization + * redrawn: + * + * <pre>m.event("click", function(d) { + * data = all.filter(function(k) k.name == d); + * vis.render(); + * });</pre> + * + * The return value of the event handler determines which mark gets re-rendered. + * Use defs ({@link #def}) to set temporary state from event handlers. + * + * <p>The complete set of event types is defined by SVG; see the reference + * below. The set of supported event types is:<ul> + * + * <li>click + * <li>mousedown + * <li>mouseup + * <li>mouseover + * <li>mousemove + * <li>mouseout + * + * </ul>Since Protovis does not specify any concept of focus, it does not + * support key events; these should be handled outside the visualization using + * standard JavaScript. In the future, support for interaction may be extended + * to support additional event types, particularly those most relevant to + * interactive visualization, such as selection. + * + * <p>TODO In the current implementation, event handlers are not inherited from + * prototype marks. They must be defined explicitly on each interactive mark. In + * addition, only one event handler for a given event type can be defined; when + * specifying multiple event handlers for the same type, only the last one will + * be used. + * + * @see <a href="http://www.w3.org/TR/SVGTiny12/interact.html#SVGEvents">SVG events</a> + * @param {string} type the event type. + * @param {function} handler the event handler. + * @returns {pv.Mark} this. + */ +pv.Mark.prototype.event = function(type, handler) { + this.$handlers[type] = pv.functor(handler); + return this; +}; + +/** @private Evaluates the function <i>f</i> with the specified context. */ +pv.Mark.prototype.context = function(scene, index, f) { + var proto = pv.Mark.prototype, + stack = pv.Mark.stack, + oscene = pv.Mark.scene, + oindex = proto.index; + + /** @private Sets the context. */ + function apply(scene, index) { + pv.Mark.scene = scene; + proto.index = index; + if (!scene) return; + + var that = scene.mark, + mark = that, + ancestors = []; + + /* Set ancestors' scene and index; populate data stack. */ + do { + ancestors.push(mark); + stack.push(scene[index].data); + mark.index = index; + mark.scene = scene; + index = scene.parentIndex; + scene = scene.parent; + } while (mark = mark.parent); + + /* Set ancestors' scale; requires top-down. */ + for (var i = ancestors.length - 1, k = 1; i > 0; i--) { + mark = ancestors[i]; + mark.scale = k; + k *= mark.scene[mark.index].transform.k; + } + + /* Set children's scene and scale. */ + if (that.children) for (var i = 0, n = that.children.length; i < n; i++) { + mark = that.children[i]; + mark.scene = that.scene[that.index].children[i]; + mark.scale = k; + } + } + + /** @private Clears the context. */ + function clear(scene, index) { + if (!scene) return; + var that = scene.mark, + mark; + + /* Reset children. */ + if (that.children) for (var i = 0, n = that.children.length; i < n; i++) { + mark = that.children[i]; + delete mark.scene; + delete mark.scale; + } + + /* Reset ancestors. */ + mark = that; + do { + stack.pop(); + if (mark.parent) { + delete mark.scene; + delete mark.scale; + } + delete mark.index; + } while (mark = mark.parent); + } + + /* Context switch, invoke the function, then switch back. */ + clear(oscene, oindex); + apply(scene, index); + try { + f.apply(this, stack); + } finally { + clear(scene, index); + apply(oscene, oindex); + } +}; + +/** @private Execute the event listener, then re-render. */ +pv.Mark.dispatch = function(type, scene, index) { + var m = scene.mark, p = scene.parent, l = m.$handlers[type]; + if (!l) return p && pv.Mark.dispatch(type, p, scene.parentIndex); + m.context(scene, index, function() { + m = l.apply(m, pv.Mark.stack); + if (m && m.render) m.render(); + }); + return true; +}; +/** + * Constructs a new mark anchor with default properties. + * + * @class Represents an anchor on a given mark. An anchor is itself a mark, but + * without a visual representation. It serves only to provide useful default + * properties that can be inherited by other marks. Each type of mark can define + * any number of named anchors for convenience. If the concrete mark type does + * not define an anchor implementation specifically, one will be inherited from + * the mark's parent class. + * + * <p>For example, the bar mark provides anchors for its four sides: left, + * right, top and bottom. Adding a label to the top anchor of a bar, + * + * <pre>bar.anchor("top").add(pv.Label);</pre> + * + * will render a text label on the top edge of the bar; the top anchor defines + * the appropriate position properties (top and left), as well as text-rendering + * properties for convenience (textAlign and textBaseline). + * + * <p>Note that anchors do not <i>inherit</i> from their targets; the positional + * properties are copied from the scene graph, which guarantees that the anchors + * are positioned correctly, even if the positional properties are not defined + * deterministically. (In addition, it also improves performance by avoiding + * re-evaluating expensive properties.) If you want the anchor to inherit from + * the target, use {@link pv.Mark#extend} before adding. For example: + * + * <pre>bar.anchor("top").extend(bar).add(pv.Label);</pre> + * + * The anchor defines it's own positional properties, but other properties (such + * as the title property, say) can be inherited using the above idiom. Also note + * that you can override positional properties in the anchor for custom + * behavior. + * + * @extends pv.Mark + * @param {pv.Mark} target the anchor target. + */ +pv.Anchor = function(target) { + pv.Mark.call(this); + this.target = target; + this.parent = target.parent; +}; + +pv.Anchor.prototype = pv.extend(pv.Mark) + .property("name", String); + +/** + * The anchor name. The set of supported anchor names is dependent on the + * concrete mark type; see the mark type for details. For example, bars support + * left, right, top and bottom anchors. + * + * <p>While anchor names are typically constants, the anchor name is a true + * property, which means you can specify a function to compute the anchor name + * dynamically. For instance, if you wanted to alternate top and bottom anchors, + * saying + * + * <pre>m.anchor(function() (this.index % 2) ? "top" : "bottom").add(pv.Dot);</pre> + * + * would have the desired effect. + * + * @type string + * @name pv.Anchor.prototype.name + */ + +/** + * Sets the prototype of this anchor to the specified mark. Any properties not + * defined on this mark may be inherited from the specified prototype mark, or + * its prototype, and so on. The prototype mark need not be the same type of + * mark as this mark. (Note that for inheritance to be useful, properties with + * the same name on different mark types should have equivalent meaning.) + * + * <p>This method differs slightly from the normal mark behavior in that the + * anchor's target is preserved. + * + * @param {pv.Mark} proto the new prototype. + * @returns {pv.Anchor} this anchor. + * @see pv.Mark#add + */ +pv.Anchor.prototype.extend = function(proto) { + this.proto = proto; + return this; +}; +/** + * Constructs a new area mark with default properties. Areas are not typically + * constructed directly, but by adding to a panel or an existing mark via + * {@link pv.Mark#add}. + * + * @class Represents an area mark: the solid area between two series of + * connected line segments. Unsurprisingly, areas are used most frequently for + * area charts. + * + * <p>Just as a line represents a polyline, the <tt>Area</tt> mark type + * represents a <i>polygon</i>. However, an area is not an arbitrary polygon; + * vertices are paired either horizontally or vertically into parallel + * <i>spans</i>, and each span corresponds to an associated datum. Either the + * width or the height must be specified, but not both; this determines whether + * the area is horizontally-oriented or vertically-oriented. Like lines, areas + * can be stroked and filled with arbitrary colors. + * + * <p>See also the <a href="../../api/Area.html">Area guide</a>. + * + * @extends pv.Mark + */ +pv.Area = function() { + pv.Mark.call(this); +}; + +pv.Area.prototype = pv.extend(pv.Mark) + .property("width", Number) + .property("height", Number) + .property("lineWidth", Number) + .property("strokeStyle", pv.color) + .property("fillStyle", pv.color) + .property("segmented", Boolean) + .property("interpolate", String) + .property("tension", Number); + +pv.Area.prototype.type = "area"; + +/** + * The width of a given span, in pixels; used for horizontal spans. If the width + * is specified, the height property should be 0 (the default). Either the top + * or bottom property should be used to space the spans vertically, typically as + * a multiple of the index. + * + * @type number + * @name pv.Area.prototype.width + */ + +/** + * The height of a given span, in pixels; used for vertical spans. If the height + * is specified, the width property should be 0 (the default). Either the left + * or right property should be used to space the spans horizontally, typically + * as a multiple of the index. + * + * @type number + * @name pv.Area.prototype.height + */ + +/** + * The width of stroked lines, in pixels; used in conjunction with + * <tt>strokeStyle</tt> to stroke the perimeter of the area. Unlike the + * {@link Line} mark type, the entire perimeter is stroked, rather than just one + * edge. The default value of this property is 1.5, but since the default stroke + * style is null, area marks are not stroked by default. + * + * <p>This property is <i>fixed</i> for non-segmented areas. See + * {@link pv.Mark}. + * + * @type number + * @name pv.Area.prototype.lineWidth + */ + +/** + * The style of stroked lines; used in conjunction with <tt>lineWidth</tt> to + * stroke the perimeter of the area. Unlike the {@link Line} mark type, the + * entire perimeter is stroked, rather than just one edge. The default value of + * this property is null, meaning areas are not stroked by default. + * + * <p>This property is <i>fixed</i> for non-segmented areas. See + * {@link pv.Mark}. + * + * @type string + * @name pv.Area.prototype.strokeStyle + * @see pv.color + */ + +/** + * The area fill style; if non-null, the interior of the polygon forming the + * area is filled with the specified color. The default value of this property + * is a categorical color. + * + * <p>This property is <i>fixed</i> for non-segmented areas. See + * {@link pv.Mark}. + * + * @type string + * @name pv.Area.prototype.fillStyle + * @see pv.color + */ + +/** + * Whether the area is segmented; whether variations in fill style, stroke + * style, and the other properties are treated as fixed. Rendering segmented + * areas is noticeably slower than non-segmented areas. + * + * <p>This property is <i>fixed</i>. See {@link pv.Mark}. + * + * @type boolean + * @name pv.Area.prototype.segmented + */ + +/** + * How to interpolate between values. Linear interpolation ("linear") is the + * default, producing a straight line between points. For piecewise constant + * functions (i.e., step functions), either "step-before" or "step-after" can be + * specified. To draw open uniform b-splines, specify "basis". To draw cardinal + * splines, specify "cardinal"; see also {@link #tension}. + * + * <p>This property is <i>fixed</i>. See {@link pv.Mark}. + * + * @type string + * @name pv.Area.prototype.interpolate + */ + +/** + * The tension of cardinal splines; used in conjunction with + * interpolate("cardinal"). A value between 0 and 1 draws cardinal splines with + * the given tension. In some sense, the tension can be interpreted as the + * "length" of the tangent; a tension of 1 will yield all zero tangents (i.e., + * linear interpolation), and a tension of 0 yields a Catmull-Rom spline. The + * default value is 0.7. + * + * <p>This property is <i>fixed</i>. See {@link pv.Mark}. + * + * @type number + * @name pv.Area.prototype.tension + */ + +/** + * Default properties for areas. By default, there is no stroke and the fill + * style is a categorical color. + * + * @type pv.Area + */ +pv.Area.prototype.defaults = new pv.Area() + .extend(pv.Mark.prototype.defaults) + .lineWidth(1.5) + .fillStyle(pv.Colors.category20().by(pv.parent)) + .interpolate("linear") + .tension(.7); + +/** @private Sets width and height to zero if null. */ +pv.Area.prototype.buildImplied = function(s) { + if (s.height == null) s.height = 0; + if (s.width == null) s.width = 0; + pv.Mark.prototype.buildImplied.call(this, s); +}; + +/** @private Records which properties may be fixed. */ +pv.Area.fixed = { + lineWidth: 1, + lineJoin: 1, + strokeStyle: 1, + fillStyle: 1, + segmented: 1, + interpolate: 1, + tension: 1 +}; + +/** + * @private Make segmented required, such that this fixed property is always + * evaluated, even if the first segment is not visible. Also cache which + * properties are normally fixed. + */ +pv.Area.prototype.bind = function() { + pv.Mark.prototype.bind.call(this); + var binds = this.binds, + required = binds.required, + optional = binds.optional; + for (var i = 0, n = optional.length; i < n; i++) { + var p = optional[i]; + p.fixed = p.name in pv.Area.fixed; + if (p.name == "segmented") { + required.push(p); + optional.splice(i, 1); + i--; + n--; + } + } + + /* Cache the original arrays so they can be restored on build. */ + this.binds.$required = required; + this.binds.$optional = optional; +}; + +/** + * @private Override the default build behavior such that fixed properties are + * determined dynamically, based on the value of the (always) fixed segmented + * property. Any fixed properties are only evaluated on the first instance, + * although their values are propagated to subsequent instances, so that they + * are available for property chaining and the like. + */ +pv.Area.prototype.buildInstance = function(s) { + var binds = this.binds; + + /* Handle fixed properties on secondary instances. */ + if (this.index) { + var fixed = binds.fixed; + + /* Determine which properties are fixed. */ + if (!fixed) { + fixed = binds.fixed = []; + function f(p) { return !p.fixed || (fixed.push(p), false); } + binds.required = binds.required.filter(f); + if (!this.scene[0].segmented) binds.optional = binds.optional.filter(f); + } + + /* Copy fixed property values from the first instance. */ + for (var i = 0, n = fixed.length; i < n; i++) { + var p = fixed[i].name; + s[p] = this.scene[0][p]; + } + } + + /* Evaluate all properties on the first instance. */ + else { + binds.required = binds.$required; + binds.optional = binds.$optional; + binds.fixed = null; + } + + pv.Mark.prototype.buildInstance.call(this, s); +}; + +/** + * Constructs a new area anchor with default properties. Areas support five + * different anchors:<ul> + * + * <li>top + * <li>left + * <li>center + * <li>bottom + * <li>right + * + * </ul>In addition to positioning properties (left, right, top bottom), the + * anchors support text rendering properties (text-align, text-baseline). Text + * is rendered to appear inside the area. The area anchor also propagates the + * interpolate, eccentricity, and tension properties such that an anchored area + * or line will match positions between control points. + * + * <p>For consistency with the other mark types, the anchor positions are + * defined in terms of their opposite edge. For example, the top anchor defines + * the bottom property, such that an area added to the top anchor grows upward. + * + * @param {string} name the anchor name; either a string or a property function. + * @returns {pv.Anchor} + */ +pv.Area.prototype.anchor = function(name) { + return pv.Mark.prototype.anchor.call(this, name) + .interpolate(function() { + return this.scene.target[this.index].interpolate; + }) + .eccentricity(function() { + return this.scene.target[this.index].eccentricity; + }) + .tension(function() { + return this.scene.target[this.index].tension; + }); +}; +/** + * Constructs a new bar mark with default properties. Bars are not typically + * constructed directly, but by adding to a panel or an existing mark via + * {@link pv.Mark#add}. + * + * @class Represents a bar: an axis-aligned rectangle that can be stroked and + * filled. Bars are used for many chart types, including bar charts, histograms + * and Gantt charts. Bars can also be used as decorations, for example to draw a + * frame border around a panel; in fact, a panel is a special type (a subclass) + * of bar. + * + * <p>Bars can be positioned in several ways. Most commonly, one of the four + * corners is fixed using two margins, and then the width and height properties + * determine the extent of the bar relative to this fixed location. For example, + * using the bottom and left properties fixes the bottom-left corner; the width + * then extends to the right, while the height extends to the top. As an + * alternative to the four corners, a bar can be positioned exclusively using + * margins; this is convenient as an inset from the containing panel, for + * example. See {@link pv.Mark} for details on the prioritization of redundant + * positioning properties. + * + * <p>See also the <a href="../../api/Bar.html">Bar guide</a>. + * + * @extends pv.Mark + */ +pv.Bar = function() { + pv.Mark.call(this); +}; + +pv.Bar.prototype = pv.extend(pv.Mark) + .property("width", Number) + .property("height", Number) + .property("lineWidth", Number) + .property("strokeStyle", pv.color) + .property("fillStyle", pv.color); + +pv.Bar.prototype.type = "bar"; + +/** + * The width of the bar, in pixels. If the left position is specified, the bar + * extends rightward from the left edge; if the right position is specified, the + * bar extends leftward from the right edge. + * + * @type number + * @name pv.Bar.prototype.width + */ + +/** + * The height of the bar, in pixels. If the bottom position is specified, the + * bar extends upward from the bottom edge; if the top position is specified, + * the bar extends downward from the top edge. + * + * @type number + * @name pv.Bar.prototype.height + */ + +/** + * The width of stroked lines, in pixels; used in conjunction with + * <tt>strokeStyle</tt> to stroke the bar's border. + * + * @type number + * @name pv.Bar.prototype.lineWidth + */ + +/** + * The style of stroked lines; used in conjunction with <tt>lineWidth</tt> to + * stroke the bar's border. The default value of this property is null, meaning + * bars are not stroked by default. + * + * @type string + * @name pv.Bar.prototype.strokeStyle + * @see pv.color + */ + +/** + * The bar fill style; if non-null, the interior of the bar is filled with the + * specified color. The default value of this property is a categorical color. + * + * @type string + * @name pv.Bar.prototype.fillStyle + * @see pv.color + */ + +/** + * Default properties for bars. By default, there is no stroke and the fill + * style is a categorical color. + * + * @type pv.Bar + */ +pv.Bar.prototype.defaults = new pv.Bar() + .extend(pv.Mark.prototype.defaults) + .lineWidth(1.5) + .fillStyle(pv.Colors.category20().by(pv.parent)); +/** + * Constructs a new dot mark with default properties. Dots are not typically + * constructed directly, but by adding to a panel or an existing mark via + * {@link pv.Mark#add}. + * + * @class Represents a dot; a dot is simply a sized glyph centered at a given + * point that can also be stroked and filled. The <tt>size</tt> property is + * proportional to the area of the rendered glyph to encourage meaningful visual + * encodings. Dots can visually encode up to eight dimensions of data, though + * this may be unwise due to integrality. See {@link pv.Mark} for details on the + * prioritization of redundant positioning properties. + * + * <p>See also the <a href="../../api/Dot.html">Dot guide</a>. + * + * @extends pv.Mark + */ +pv.Dot = function() { + pv.Mark.call(this); +}; + +pv.Dot.prototype = pv.extend(pv.Mark) + .property("size", Number) + .property("radius", Number) + .property("shape", String) + .property("angle", Number) + .property("lineWidth", Number) + .property("strokeStyle", pv.color) + .property("fillStyle", pv.color); + +pv.Dot.prototype.type = "dot"; + +/** + * The size of the dot, in square pixels. Square pixels are used such that the + * area of the dot is linearly proportional to the value of the size property, + * facilitating representative encodings. + * + * @see #radius + * @type number + * @name pv.Dot.prototype.size + */ + +/** + * The radius of the dot, in pixels. This is an alternative to using + * {@link #size}. + * + * @see #size + * @type number + * @name pv.Dot.prototype.radius + */ + +/** + * The shape name. Several shapes are supported:<ul> + * + * <li>cross + * <li>triangle + * <li>diamond + * <li>square + * <li>circle + * <li>tick + * <li>bar + * + * </ul>These shapes can be further changed using the {@link #angle} property; + * for instance, a cross can be turned into a plus by rotating. Similarly, the + * tick, which is vertical by default, can be rotated horizontally. Note that + * some shapes (cross and tick) do not have interior areas, and thus do not + * support fill style meaningfully. + * + * <p>Note: it may be more natural to use the {@link pv.Rule} mark for + * horizontal and vertical ticks. The tick shape is only necessary if angled + * ticks are needed. + * + * @type string + * @name pv.Dot.prototype.shape + */ + +/** + * The rotation angle, in radians. Used to rotate shapes, such as to turn a + * cross into a plus. + * + * @type number + * @name pv.Dot.prototype.angle + */ + +/** + * The width of stroked lines, in pixels; used in conjunction with + * <tt>strokeStyle</tt> to stroke the dot's shape. + * + * @type number + * @name pv.Dot.prototype.lineWidth + */ + +/** + * The style of stroked lines; used in conjunction with <tt>lineWidth</tt> to + * stroke the dot's shape. The default value of this property is a categorical + * color. + * + * @type string + * @name pv.Dot.prototype.strokeStyle + * @see pv.color + */ + +/** + * The fill style; if non-null, the interior of the dot is filled with the + * specified color. The default value of this property is null, meaning dots are + * not filled by default. + * + * @type string + * @name pv.Dot.prototype.fillStyle + * @see pv.color + */ + +/** + * Default properties for dots. By default, there is no fill and the stroke + * style is a categorical color. The default shape is "circle" with size 20. + * + * @type pv.Dot + */ +pv.Dot.prototype.defaults = new pv.Dot() + .extend(pv.Mark.prototype.defaults) + .size(20) + .shape("circle") + .lineWidth(1.5) + .strokeStyle(pv.Colors.category10().by(pv.parent)); + +/** + * Constructs a new dot anchor with default properties. Dots support five + * different anchors:<ul> + * + * <li>top + * <li>left + * <li>center + * <li>bottom + * <li>right + * + * </ul>In addition to positioning properties (left, right, top bottom), the + * anchors support text rendering properties (text-align, text-baseline). Text is + * rendered to appear outside the dot. Note that this behavior is different from + * other mark anchors, which default to rendering text <i>inside</i> the mark. + * + * <p>For consistency with the other mark types, the anchor positions are + * defined in terms of their opposite edge. For example, the top anchor defines + * the bottom property, such that a bar added to the top anchor grows upward. + * + * @param {string} name the anchor name; either a string or a property function. + * @returns {pv.Anchor} + */ +pv.Dot.prototype.anchor = function(name) { + return pv.Mark.prototype.anchor.call(this, name) + .left(function() { + var s = this.scene.target[this.index]; + switch (this.name()) { + case "bottom": + case "top": + case "center": return s.left; + case "left": return null; + } + return s.left + s.radius; + }) + .right(function() { + var s = this.scene.target[this.index]; + return this.name() == "left" ? s.right + s.radius : null; + }) + .top(function() { + var s = this.scene.target[this.index]; + switch (this.name()) { + case "left": + case "right": + case "center": return s.top; + case "top": return null; + } + return s.top + s.radius; + }) + .bottom(function() { + var s = this.scene.target[this.index]; + return this.name() == "top" ? s.bottom + s.radius : null; + }) + .textAlign(function() { + switch (this.name()) { + case "left": return "right"; + case "bottom": + case "top": + case "center": return "center"; + } + return "left"; + }) + .textBaseline(function() { + switch (this.name()) { + case "right": + case "left": + case "center": return "middle"; + case "bottom": return "top"; + } + return "bottom"; + }); +}; + +/** @private Sets radius based on size or vice versa. */ +pv.Dot.prototype.buildImplied = function(s) { + if (s.radius == null) s.radius = Math.sqrt(s.size); + else if (s.size == null) s.size = s.radius * s.radius; + pv.Mark.prototype.buildImplied.call(this, s); +}; +/** + * Constructs a new label mark with default properties. Labels are not typically + * constructed directly, but by adding to a panel or an existing mark via + * {@link pv.Mark#add}. + * + * @class Represents a text label, allowing textual annotation of other marks or + * arbitrary text within the visualization. The character data must be plain + * text (unicode), though the text can be styled using the {@link #font} + * property. If rich text is needed, external HTML elements can be overlaid on + * the canvas by hand. + * + * <p>Labels are positioned using the box model, similarly to {@link Dot}. Thus, + * a label has no width or height, but merely a text anchor location. The text + * is positioned relative to this anchor location based on the + * {@link #textAlign}, {@link #textBaseline} and {@link #textMargin} properties. + * Furthermore, the text may be rotated using {@link #textAngle}. + * + * <p>Labels ignore events, so as to not interfere with event handlers on + * underlying marks, such as bars. In the future, we may support event handlers + * on labels. + * + * <p>See also the <a href="../../api/Label.html">Label guide</a>. + * + * @extends pv.Mark + */ +pv.Label = function() { + pv.Mark.call(this); +}; + +pv.Label.prototype = pv.extend(pv.Mark) + .property("text", String) + .property("font", String) + .property("textAngle", Number) + .property("textStyle", pv.color) + .property("textAlign", String) + .property("textBaseline", String) + .property("textMargin", Number) + .property("textDecoration", String) + .property("textShadow", String); + +pv.Label.prototype.type = "label"; + +/** + * The character data to render; a string. The default value of the text + * property is the identity function, meaning the label's associated datum will + * be rendered using its <tt>toString</tt>. + * + * @type string + * @name pv.Label.prototype.text + */ + +/** + * The font format, per the CSS Level 2 specification. The default font is "10px + * sans-serif", for consistency with the HTML 5 canvas element specification. + * Note that since text is not wrapped, any line-height property will be + * ignored. The other font-style, font-variant, font-weight, font-size and + * font-family properties are supported. + * + * @see <a href="http://www.w3.org/TR/CSS2/fonts.html#font-shorthand">CSS2 fonts</a> + * @type string + * @name pv.Label.prototype.font + */ + +/** + * The rotation angle, in radians. Text is rotated clockwise relative to the + * anchor location. For example, with the default left alignment, an angle of + * Math.PI / 2 causes text to proceed downwards. The default angle is zero. + * + * @type number + * @name pv.Label.prototype.textAngle + */ + +/** + * The text color. The name "textStyle" is used for consistency with "fillStyle" + * and "strokeStyle", although it might be better to rename this property (and + * perhaps use the same name as "strokeStyle"). The default color is black. + * + * @type string + * @name pv.Label.prototype.textStyle + * @see pv.color + */ + +/** + * The horizontal text alignment. One of:<ul> + * + * <li>left + * <li>center + * <li>right + * + * </ul>The default horizontal alignment is left. + * + * @type string + * @name pv.Label.prototype.textAlign + */ + +/** + * The vertical text alignment. One of:<ul> + * + * <li>top + * <li>middle + * <li>bottom + * + * </ul>The default vertical alignment is bottom. + * + * @type string + * @name pv.Label.prototype.textBaseline + */ + +/** + * The text margin; may be specified in pixels, or in font-dependent units (such + * as ".1ex"). The margin can be used to pad text away from its anchor location, + * in a direction dependent on the horizontal and vertical alignment + * properties. For example, if the text is left- and middle-aligned, the margin + * shifts the text to the right. The default margin is 3 pixels. + * + * @type number + * @name pv.Label.prototype.textMargin + */ + +/** + * A list of shadow effects to be applied to text, per the CSS Text Level 3 + * text-shadow property. An example specification is "0.1em 0.1em 0.1em + * rgba(0,0,0,.5)"; the first length is the horizontal offset, the second the + * vertical offset, and the third the blur radius. + * + * @see <a href="http://www.w3.org/TR/css3-text/#text-shadow">CSS3 text</a> + * @type string + * @name pv.Label.prototype.textShadow + */ + +/** + * A list of decoration to be applied to text, per the CSS Text Level 3 + * text-decoration property. An example specification is "underline". + * + * @see <a href="http://www.w3.org/TR/css3-text/#text-decoration">CSS3 text</a> + * @type string + * @name pv.Label.prototype.textDecoration + */ + +/** + * Default properties for labels. See the individual properties for the default + * values. + * + * @type pv.Label + */ +pv.Label.prototype.defaults = new pv.Label() + .extend(pv.Mark.prototype.defaults) + .events("none") + .text(pv.identity) + .font("10px sans-serif") + .textAngle(0) + .textStyle("black") + .textAlign("left") + .textBaseline("bottom") + .textMargin(3); +/** + * Constructs a new line mark with default properties. Lines are not typically + * constructed directly, but by adding to a panel or an existing mark via + * {@link pv.Mark#add}. + * + * @class Represents a series of connected line segments, or <i>polyline</i>, + * that can be stroked with a configurable color and thickness. Each + * articulation point in the line corresponds to a datum; for <i>n</i> points, + * <i>n</i>-1 connected line segments are drawn. The point is positioned using + * the box model. Arbitrary paths are also possible, allowing radar plots and + * other custom visualizations. + * + * <p>Like areas, lines can be stroked and filled with arbitrary colors. In most + * cases, lines are only stroked, but the fill style can be used to construct + * arbitrary polygons. + * + * <p>See also the <a href="../../api/Line.html">Line guide</a>. + * + * @extends pv.Mark + */ +pv.Line = function() { + pv.Mark.call(this); +}; + +pv.Line.prototype = pv.extend(pv.Mark) + .property("lineWidth", Number) + .property("lineJoin", String) + .property("strokeStyle", pv.color) + .property("fillStyle", pv.color) + .property("segmented", Boolean) + .property("interpolate", String) + .property("eccentricity", Number) + .property("tension", Number); + +pv.Line.prototype.type = "line"; + +/** + * The width of stroked lines, in pixels; used in conjunction with + * <tt>strokeStyle</tt> to stroke the line. + * + * @type number + * @name pv.Line.prototype.lineWidth + */ + +/** + * The style of stroked lines; used in conjunction with <tt>lineWidth</tt> to + * stroke the line. The default value of this property is a categorical color. + * + * @type string + * @name pv.Line.prototype.strokeStyle + * @see pv.color + */ + +/** + * The type of corners where two lines meet. Accepted values are "bevel", + * "round" and "miter". The default value is "miter". + * + * <p>For segmented lines, only "miter" joins and "linear" interpolation are + * currently supported. Any other value, including null, will disable joins, + * producing disjoint line segments. Note that the miter joins must be computed + * manually (at least in the current SVG renderer); since this calculation may + * be expensive and unnecessary for small lines, specifying null can improve + * performance significantly. + * + * <p>This property is <i>fixed</i>. See {@link pv.Mark}. + * + * @type string + * @name pv.Line.prototype.lineJoin + */ + +/** + * The line fill style; if non-null, the interior of the line is closed and + * filled with the specified color. The default value of this property is a + * null, meaning that lines are not filled by default. + * + * <p>This property is <i>fixed</i>. See {@link pv.Mark}. + * + * @type string + * @name pv.Line.prototype.fillStyle + * @see pv.color + */ + +/** + * Whether the line is segmented; whether variations in stroke style, line width + * and the other properties are treated as fixed. Rendering segmented lines is + * noticeably slower than non-segmented lines. + * + * <p>This property is <i>fixed</i>. See {@link pv.Mark}. + * + * @type boolean + * @name pv.Line.prototype.segmented + */ + +/** + * How to interpolate between values. Linear interpolation ("linear") is the + * default, producing a straight line between points. For piecewise constant + * functions (i.e., step functions), either "step-before" or "step-after" can be + * specified. To draw a clockwise circular arc between points, specify "polar"; + * to draw a counterclockwise circular arc between points, specify + * "polar-reverse". To draw open uniform b-splines, specify "basis". To draw + * cardinal splines, specify "cardinal"; see also {@link #tension}. + * + * <p>This property is <i>fixed</i>. See {@link pv.Mark}. + * + * @type string + * @name pv.Line.prototype.interpolate + */ + +/** + * The eccentricity of polar line segments; used in conjunction with + * interpolate("polar"). The default value of 0 means that line segments are + * drawn as circular arcs. A value of 1 draws a straight line. A value between 0 + * and 1 draws an elliptical arc with the given eccentricity. + * + * @type number + * @name pv.Line.prototype.eccentricity + */ + +/** + * The tension of cardinal splines; used in conjunction with + * interpolate("cardinal"). A value between 0 and 1 draws cardinal splines with + * the given tension. In some sense, the tension can be interpreted as the + * "length" of the tangent; a tension of 1 will yield all zero tangents (i.e., + * linear interpolation), and a tension of 0 yields a Catmull-Rom spline. The + * default value is 0.7. + * + * <p>This property is <i>fixed</i>. See {@link pv.Mark}. + * + * @type number + * @name pv.Line.prototype.tension + */ + +/** + * Default properties for lines. By default, there is no fill and the stroke + * style is a categorical color. The default interpolation is linear. + * + * @type pv.Line + */ +pv.Line.prototype.defaults = new pv.Line() + .extend(pv.Mark.prototype.defaults) + .lineJoin("miter") + .lineWidth(1.5) + .strokeStyle(pv.Colors.category10().by(pv.parent)) + .interpolate("linear") + .eccentricity(0) + .tension(.7); + +/** @private Reuse Area's implementation for segmented bind & build. */ +pv.Line.prototype.bind = pv.Area.prototype.bind; +pv.Line.prototype.buildInstance = pv.Area.prototype.buildInstance; + +/** + * Constructs a new line anchor with default properties. Lines support five + * different anchors:<ul> + * + * <li>top + * <li>left + * <li>center + * <li>bottom + * <li>right + * + * </ul>In addition to positioning properties (left, right, top bottom), the + * anchors support text rendering properties (text-align, text-baseline). Text is + * rendered to appear outside the line. Note that this behavior is different + * from other mark anchors, which default to rendering text <i>inside</i> the + * mark. + * + * <p>For consistency with the other mark types, the anchor positions are + * defined in terms of their opposite edge. For example, the top anchor defines + * the bottom property, such that a bar added to the top anchor grows upward. + * + * @param {string} name the anchor name; either a string or a property function. + * @returns {pv.Anchor} + */ +pv.Line.prototype.anchor = function(name) { + return pv.Area.prototype.anchor.call(this, name) + .textAlign(function(d) { + switch (this.name()) { + case "left": return "right"; + case "bottom": + case "top": + case "center": return "center"; + case "right": return "left"; + } + }) + .textBaseline(function(d) { + switch (this.name()) { + case "right": + case "left": + case "center": return "middle"; + case "top": return "bottom"; + case "bottom": return "top"; + } + }); +}; +/** + * Constructs a new rule with default properties. Rules are not typically + * constructed directly, but by adding to a panel or an existing mark via + * {@link pv.Mark#add}. + * + * @class Represents a horizontal or vertical rule. Rules are frequently used + * for axes and grid lines. For example, specifying only the bottom property + * draws horizontal rules, while specifying only the left draws vertical + * rules. Rules can also be used as thin bars. The visual style is controlled in + * the same manner as lines. + * + * <p>Rules are positioned exclusively the standard box model properties. The + * following combinations of properties are supported: + * + * <table> + * <thead><th style="width:12em;">Properties</th><th>Orientation</th></thead> + * <tbody> + * <tr><td>left</td><td>vertical</td></tr> + * <tr><td>right</td><td>vertical</td></tr> + * <tr><td>left, bottom, top</td><td>vertical</td></tr> + * <tr><td>right, bottom, top</td><td>vertical</td></tr> + * <tr><td>top</td><td>horizontal</td></tr> + * <tr><td>bottom</td><td>horizontal</td></tr> + * <tr><td>top, left, right</td><td>horizontal</td></tr> + * <tr><td>bottom, left, right</td><td>horizontal</td></tr> + * <tr><td>left, top, height</td><td>vertical</td></tr> + * <tr><td>left, bottom, height</td><td>vertical</td></tr> + * <tr><td>right, top, height</td><td>vertical</td></tr> + * <tr><td>right, bottom, height</td><td>vertical</td></tr> + * <tr><td>left, top, width</td><td>horizontal</td></tr> + * <tr><td>left, bottom, width</td><td>horizontal</td></tr> + * <tr><td>right, top, width</td><td>horizontal</td></tr> + * <tr><td>right, bottom, width</td><td>horizontal</td></tr> + * </tbody> + * </table> + * + * <p>Small rules can be used as tick marks; alternatively, a {@link Dot} with + * the "tick" shape can be used. + * + * <p>See also the <a href="../../api/Rule.html">Rule guide</a>. + * + * @see pv.Line + * @extends pv.Mark + */ +pv.Rule = function() { + pv.Mark.call(this); +}; + +pv.Rule.prototype = pv.extend(pv.Mark) + .property("width", Number) + .property("height", Number) + .property("lineWidth", Number) + .property("strokeStyle", pv.color); + +pv.Rule.prototype.type = "rule"; + +/** + * The width of the rule, in pixels. If the left position is specified, the rule + * extends rightward from the left edge; if the right position is specified, the + * rule extends leftward from the right edge. + * + * @type number + * @name pv.Rule.prototype.width + */ + +/** + * The height of the rule, in pixels. If the bottom position is specified, the + * rule extends upward from the bottom edge; if the top position is specified, + * the rule extends downward from the top edge. + * + * @type number + * @name pv.Rule.prototype.height + */ + +/** + * The width of stroked lines, in pixels; used in conjunction with + * <tt>strokeStyle</tt> to stroke the rule. The default value is 1 pixel. + * + * @type number + * @name pv.Rule.prototype.lineWidth + */ + +/** + * The style of stroked lines; used in conjunction with <tt>lineWidth</tt> to + * stroke the rule. The default value of this property is black. + * + * @type string + * @name pv.Rule.prototype.strokeStyle + * @see pv.color + */ + +/** + * Default properties for rules. By default, a single-pixel black line is + * stroked. + * + * @type pv.Rule + */ +pv.Rule.prototype.defaults = new pv.Rule() + .extend(pv.Mark.prototype.defaults) + .lineWidth(1) + .strokeStyle("black") + .antialias(false); + +/** + * Constructs a new rule anchor with default properties. Rules support five + * different anchors:<ul> + * + * <li>top + * <li>left + * <li>center + * <li>bottom + * <li>right + * + * </ul>In addition to positioning properties (left, right, top bottom), the + * anchors support text rendering properties (text-align, text-baseline). Text is + * rendered to appear outside the rule. Note that this behavior is different + * from other mark anchors, which default to rendering text <i>inside</i> the + * mark. + * + * <p>For consistency with the other mark types, the anchor positions are + * defined in terms of their opposite edge. For example, the top anchor defines + * the bottom property, such that a bar added to the top anchor grows upward. + * + * @param {string} name the anchor name; either a string or a property function. + * @returns {pv.Anchor} + */ +pv.Rule.prototype.anchor = pv.Line.prototype.anchor; + +/** @private Sets width or height based on orientation. */ +pv.Rule.prototype.buildImplied = function(s) { + var l = s.left, r = s.right, t = s.top, b = s.bottom; + + /* Determine horizontal or vertical orientation. */ + if ((s.width != null) + || ((l == null) && (r == null)) + || ((r != null) && (l != null))) { + s.height = 0; + } else { + s.width = 0; + } + + pv.Mark.prototype.buildImplied.call(this, s); +}; +/** + * Constructs a new, empty panel with default properties. Panels, with the + * exception of the root panel, are not typically constructed directly; instead, + * they are added to an existing panel or mark via {@link pv.Mark#add}. + * + * @class Represents a container mark. Panels allow repeated or nested + * structures, commonly used in small multiple displays where a small + * visualization is tiled to facilitate comparison across one or more + * dimensions. Other types of visualizations may benefit from repeated and + * possibly overlapping structure as well, such as stacked area charts. Panels + * can also offset the position of marks to provide padding from surrounding + * content. + * + * <p>All Protovis displays have at least one panel; this is the root panel to + * which marks are rendered. The box model properties (four margins, width and + * height) are used to offset the positions of contained marks. The data + * property determines the panel count: a panel is generated once per associated + * datum. When nested panels are used, property functions can declare additional + * arguments to access the data associated with enclosing panels. + * + * <p>Panels can be rendered inline, facilitating the creation of sparklines. + * This allows designers to reuse browser layout features, such as text flow and + * tables; designers can also overlay HTML elements such as rich text and + * images. + * + * <p>All panels have a <tt>children</tt> array (possibly empty) containing the + * child marks in the order they were added. Panels also have a <tt>root</tt> + * field which points to the root (outermost) panel; the root panel's root field + * points to itself. + * + * <p>See also the <a href="../../api/">Protovis guide</a>. + * + * @extends pv.Bar + */ +pv.Panel = function() { + pv.Bar.call(this); + + /** + * The child marks; zero or more {@link pv.Mark}s in the order they were + * added. + * + * @see #add + * @type pv.Mark[] + */ + this.children = []; + this.root = this; + + /** + * The internal $dom field is set by the Protovis loader; see lang/init.js. It + * refers to the script element that contains the Protovis specification, so + * that the panel knows where in the DOM to insert the generated SVG element. + * + * @private + */ + this.$dom = pv.$ && pv.$.s; +}; + +pv.Panel.prototype = pv.extend(pv.Bar) + .property("transform") + .property("overflow", String) + .property("canvas", function(c) { + return (typeof c == "string") + ? document.getElementById(c) + : c; // assume that c is the passed-in element + }); + +pv.Panel.prototype.type = "panel"; + +/** + * The canvas element; either the string ID of the canvas element in the current + * document, or a reference to the canvas element itself. If null, a canvas + * element will be created and inserted into the document at the location of the + * script element containing the current Protovis specification. This property + * only applies to root panels and is ignored on nested panels. + * + * <p>Note: the "canvas" element here refers to a <tt>div</tt> (or other suitable + * HTML container element), <i>not</i> a <tt>canvas</tt> element. The name of + * this property is a historical anachronism from the first implementation that + * used HTML 5 canvas, rather than SVG. + * + * @type string + * @name pv.Panel.prototype.canvas + */ + +/** + * Specifies whether child marks are clipped when they overflow this panel. + * This affects the clipping of all this panel's descendant marks. + * + * @type string + * @name pv.Panel.prototype.overflow + * @see <a href="http://www.w3.org/TR/CSS2/visufx.html#overflow">CSS2</a> + */ + +/** + * The transform to be applied to child marks. The default transform is + * identity, which has no effect. Note that the panel's own fill and stroke are + * not affected by the transform, and panel's transform only affects the + * <tt>scale</tt> of child marks, not the panel itself. + * + * @type pv.Transform + * @name pv.Panel.prototype.transform + * @see pv.Mark#scale + */ + +/** + * Default properties for panels. By default, the margins are zero, the fill + * style is transparent. + * + * @type pv.Panel + */ +pv.Panel.prototype.defaults = new pv.Panel() + .extend(pv.Bar.prototype.defaults) + .fillStyle(null) // override Bar default + .overflow("visible"); + +/** + * Returns an anchor with the specified name. This method is overridden such + * that adding to a panel's anchor adds to the panel, rather than to the panel's + * parent. + * + * @param {string} name the anchor name; either a string or a property function. + * @returns {pv.Anchor} the new anchor. + */ +pv.Panel.prototype.anchor = function(name) { + var anchor = pv.Bar.prototype.anchor.call(this, name); + anchor.parent = this; + return anchor; +}; + +/** + * Adds a new mark of the specified type to this panel. Unlike the normal + * {@link Mark#add} behavior, adding a mark to a panel does not cause the mark + * to inherit from the panel. Since the contained marks are offset by the panel + * margins already, inheriting properties is generally undesirable; of course, + * it is always possible to change this behavior by calling {@link Mark#extend} + * explicitly. + * + * @param {function} type the type of the new mark to add. + * @returns {pv.Mark} the new mark. + */ +pv.Panel.prototype.add = function(type) { + var child = new type(); + child.parent = this; + child.root = this.root; + child.childIndex = this.children.length; + this.children.push(child); + return child; +}; + +/** @private Bind this panel, then any child marks recursively. */ +pv.Panel.prototype.bind = function() { + pv.Mark.prototype.bind.call(this); + for (var i = 0; i < this.children.length; i++) { + this.children[i].bind(); + } +}; + +/** + * @private Evaluates all of the properties for this panel for the specified + * instance <tt>s</tt> in the scene graph, including recursively building the + * scene graph for child marks. + * + * @param s a node in the scene graph; the instance of the panel to build. + * @see Mark#scene + */ +pv.Panel.prototype.buildInstance = function(s) { + pv.Bar.prototype.buildInstance.call(this, s); + if (!s.visible) return; + if (!s.children) s.children = []; + + /* + * Multiply the current scale factor by this panel's transform. Also clear the + * default index as we recurse into child marks; it will be reset to the + * current index when the next panel instance is built. + */ + var scale = this.scale * s.transform.k, child, n = this.children.length; + pv.Mark.prototype.index = -1; + + /* + * Build each child, passing in the parent (this panel) scene graph node. The + * child mark's scene is initialized from the corresponding entry in the + * existing scene graph, such that properties from the previous build can be + * reused; this is largely to facilitate the recycling of SVG elements. + */ + for (var i = 0; i < n; i++) { + child = this.children[i]; + child.scene = s.children[i]; // possibly undefined + child.scale = scale; + child.build(); + } + + /* + * Once the child marks have been built, the new scene graph nodes are removed + * from the child marks and placed into the scene graph. The nodes cannot + * remain on the child nodes because this panel (or a parent panel) may be + * instantiated multiple times! + */ + for (var i = 0; i < n; i++) { + child = this.children[i]; + s.children[i] = child.scene; + delete child.scene; + delete child.scale; + } + + /* Delete any expired child scenes. */ + s.children.length = n; +}; + +/** + * @private Computes the implied properties for this panel for the specified + * instance <tt>s</tt> in the scene graph. Panels have two implied + * properties:<ul> + * + * <li>The <tt>canvas</tt> property references the DOM element, typically a DIV, + * that contains the SVG element that is used to display the visualization. This + * property may be specified as a string, referring to the unique ID of the + * element in the DOM. The string is converted to a reference to the DOM + * element. The width and height of the SVG element is inferred from this DOM + * element. If no canvas property is specified, a new SVG element is created and + * inserted into the document, using the panel dimensions; see + * {@link #createCanvas}. + * + * <li>The <tt>children</tt> array, while not a property per se, contains the + * scene graph for each child mark. This array is initialized to be empty, and + * is populated above in {@link #buildInstance}. + * + * </ul>The current implementation creates the SVG element, if necessary, during + * the build phase; in the future, it may be preferrable to move this to the + * update phase, although then the canvas property would be undefined. In + * addition, DOM inspection is necessary to define the implied width and height + * properties that may be inferred from the DOM. + * + * @param s a node in the scene graph; the instance of the panel to build. + */ +pv.Panel.prototype.buildImplied = function(s) { + if (!this.parent) { + var c = s.canvas; + if (c) { + /* Clear the container if it's not associated with this panel. */ + if (c.$panel != this) { + c.$panel = this; + while (c.lastChild) c.removeChild(c.lastChild); + } + + /* If width and height weren't specified, inspect the container. */ + var w, h; + if (s.width == null) { + w = parseFloat(pv.css(c, "width")); + s.width = w - s.left - s.right; + } + if (s.height == null) { + h = parseFloat(pv.css(c, "height")); + s.height = h - s.top - s.bottom; + } + } else { + var cache = this.$canvas || (this.$canvas = []); + if (!(c = cache[this.index])) { + c = cache[this.index] = document.createElement("span"); + if (this.$dom) { // script element for text/javascript+protovis + this.$dom.parentNode.insertBefore(c, this.$dom); + } else { // find the last element in the body + var n = document.body; + while (n.lastChild && n.lastChild.tagName) n = n.lastChild; + if (n != document.body) n = n.parentNode; + n.appendChild(c); + } + } + } + s.canvas = c; + } + if (!s.transform) s.transform = pv.Transform.identity; + pv.Mark.prototype.buildImplied.call(this, s); +}; +/** + * Constructs a new image with default properties. Images are not typically + * constructed directly, but by adding to a panel or an existing mark via + * {@link pv.Mark#add}. + * + * @class Represents an image, either a static resource or a dynamically- + * generated pixel buffer. Images share the same layout and style properties as + * bars. The external image resource is specified via the {@link #url} + * property. The optional fill, if specified, appears beneath the image, while + * the optional stroke appears above the image. + * + * <p>Dynamic images such as heatmaps are supported using the {@link #image} + * psuedo-property. This function is passed the <i>x</i> and <i>y</i> index, in + * addition to the current data stack. The return value is a {@link pv.Color}, + * or null for transparent. A string can also be returned, which will be parsed + * into a color; however, it is typically much faster to return an object with + * <tt>r</tt>, <tt>g</tt>, <tt>b</tt> and <tt>a</tt> attributes, to avoid the + * cost of parsing and object instantiation. + * + * <p>See {@link pv.Bar} for details on positioning properties. + * + * @extends pv.Bar + */ +pv.Image = function() { + pv.Bar.call(this); +}; + +pv.Image.prototype = pv.extend(pv.Bar) + .property("url", String) + .property("imageWidth", Number) + .property("imageHeight", Number); + +pv.Image.prototype.type = "image"; + +/** + * The URL of the image to display. The set of supported image types is + * browser-dependent; PNG and JPEG are recommended. + * + * @type string + * @name pv.Image.prototype.url + */ + +/** + * The width of the image in pixels. For static images, this property is + * computed implicitly from the loaded image resources. For dynamic images, this + * property can be used to specify the width of the pixel buffer; otherwise, the + * value is derived from the <tt>width</tt> property. + * + * @type number + * @name pv.Image.prototype.imageWidth + */ + +/** + * The height of the image in pixels. For static images, this property is + * computed implicitly from the loaded image resources. For dynamic images, this + * property can be used to specify the height of the pixel buffer; otherwise, the + * value is derived from the <tt>height</tt> property. + * + * @type number + * @name pv.Image.prototype.imageHeight + */ + +/** + * Default properties for images. By default, there is no stroke or fill style. + * + * @type pv.Image + */ +pv.Image.prototype.defaults = new pv.Image() + .extend(pv.Bar.prototype.defaults) + .fillStyle(null); + +/** + * Specifies the dynamic image function. By default, no image function is + * specified and the <tt>url</tt> property is used to load a static image + * resource. If an image function is specified, it will be invoked for each + * pixel in the image, based on the related <tt>imageWidth</tt> and + * <tt>imageHeight</tt> properties. + * + * <p>For example, given a two-dimensional array <tt>heatmap</tt>, containing + * numbers in the range [0, 1] in row-major order, a simple monochrome heatmap + * image can be specified as: + * + * <pre>vis.add(pv.Image) + * .imageWidth(heatmap[0].length) + * .imageHeight(heatmap.length) + * .image(pv.ramp("white", "black").by(function(x, y) heatmap[y][x]));</pre> + * + * For fastest performance, use an ordinal scale which caches the fixed color + * palette, or return an object literal with <tt>r</tt>, <tt>g</tt>, <tt>b</tt> + * and <tt>a</tt> attributes. A {@link pv.Color} or string can also be returned, + * though this typically results in slower performance. + * + * @param {function} f the new sizing function. + * @returns {pv.Layout.Pack} this. + */ +pv.Image.prototype.image = function(f) { + /** @private */ + this.$image = function() { + var c = f.apply(this, arguments); + return c == null ? pv.Color.transparent + : typeof c == "string" ? pv.color(c) + : c; + }; + return this; +}; + +/** @private Scan the proto chain for an image function. */ +pv.Image.prototype.bind = function() { + pv.Bar.prototype.bind.call(this); + var binds = this.binds, mark = this; + do { + binds.image = mark.$image; + } while (!binds.image && (mark = mark.proto)); +}; + +/** @private */ +pv.Image.prototype.buildImplied = function(s) { + pv.Bar.prototype.buildImplied.call(this, s); + if (!s.visible) return; + + /* Compute the implied image dimensions. */ + if (s.imageWidth == null) s.imageWidth = s.width; + if (s.imageHeight == null) s.imageHeight = s.height; + + /* Compute the pixel values. */ + if ((s.url == null) && this.binds.image) { + + /* Cache the canvas element to reuse across renders. */ + var canvas = this.$canvas || (this.$canvas = document.createElement("canvas")), + context = canvas.getContext("2d"), + w = s.imageWidth, + h = s.imageHeight, + stack = pv.Mark.stack, + data; + + /* Evaluate the image function, storing into a CanvasPixelArray. */ + canvas.width = w; + canvas.height = h; + data = (s.image = context.createImageData(w, h)).data; + stack.unshift(null, null); + for (var y = 0, p = 0; y < h; y++) { + stack[1] = y; + for (var x = 0; x < w; x++) { + stack[0] = x; + var color = this.binds.image.apply(this, stack); + data[p++] = color.r; + data[p++] = color.g; + data[p++] = color.b; + data[p++] = 255 * color.a; + } + } + stack.splice(0, 2); + } +}; +/** + * Constructs a new wedge with default properties. Wedges are not typically + * constructed directly, but by adding to a panel or an existing mark via + * {@link pv.Mark#add}. + * + * @class Represents a wedge, or pie slice. Specified in terms of start and end + * angle, inner and outer radius, wedges can be used to construct donut charts + * and polar bar charts as well. If the {@link #angle} property is used, the end + * angle is implied by adding this value to start angle. By default, the start + * angle is the previously-generated wedge's end angle. This design allows + * explicit control over the wedge placement if desired, while offering + * convenient defaults for the construction of radial graphs. + * + * <p>The center point of the circle is positioned using the standard box model. + * The wedge can be stroked and filled, similar to {@link pv.Bar}. + * + * <p>See also the <a href="../../api/Wedge.html">Wedge guide</a>. + * + * @extends pv.Mark + */ +pv.Wedge = function() { + pv.Mark.call(this); +}; + +pv.Wedge.prototype = pv.extend(pv.Mark) + .property("startAngle", Number) + .property("endAngle", Number) + .property("angle", Number) + .property("innerRadius", Number) + .property("outerRadius", Number) + .property("lineWidth", Number) + .property("strokeStyle", pv.color) + .property("fillStyle", pv.color); + +pv.Wedge.prototype.type = "wedge"; + +/** + * The start angle of the wedge, in radians. The start angle is measured + * clockwise from the 3 o'clock position. The default value of this property is + * the end angle of the previous instance (the {@link Mark#sibling}), or -PI / 2 + * for the first wedge; for pie and donut charts, typically only the + * {@link #angle} property needs to be specified. + * + * @type number + * @name pv.Wedge.prototype.startAngle + */ + +/** + * The end angle of the wedge, in radians. If not specified, the end angle is + * implied as the start angle plus the {@link #angle}. + * + * @type number + * @name pv.Wedge.prototype.endAngle + */ + +/** + * The angular span of the wedge, in radians. This property is used if end angle + * is not specified. + * + * @type number + * @name pv.Wedge.prototype.angle + */ + +/** + * The inner radius of the wedge, in pixels. The default value of this property + * is zero; a positive value will produce a donut slice rather than a pie slice. + * The inner radius can vary per-wedge. + * + * @type number + * @name pv.Wedge.prototype.innerRadius + */ + +/** + * The outer radius of the wedge, in pixels. This property is required. For + * pies, only this radius is required; for donuts, the inner radius must be + * specified as well. The outer radius can vary per-wedge. + * + * @type number + * @name pv.Wedge.prototype.outerRadius + */ + +/** + * The width of stroked lines, in pixels; used in conjunction with + * <tt>strokeStyle</tt> to stroke the wedge's border. + * + * @type number + * @name pv.Wedge.prototype.lineWidth + */ + +/** + * The style of stroked lines; used in conjunction with <tt>lineWidth</tt> to + * stroke the wedge's border. The default value of this property is null, + * meaning wedges are not stroked by default. + * + * @type string + * @name pv.Wedge.prototype.strokeStyle + * @see pv.color + */ + +/** + * The wedge fill style; if non-null, the interior of the wedge is filled with + * the specified color. The default value of this property is a categorical + * color. + * + * @type string + * @name pv.Wedge.prototype.fillStyle + * @see pv.color + */ + +/** + * Default properties for wedges. By default, there is no stroke and the fill + * style is a categorical color. + * + * @type pv.Wedge + */ +pv.Wedge.prototype.defaults = new pv.Wedge() + .extend(pv.Mark.prototype.defaults) + .startAngle(function() { + var s = this.sibling(); + return s ? s.endAngle : -Math.PI / 2; + }) + .innerRadius(0) + .lineWidth(1.5) + .strokeStyle(null) + .fillStyle(pv.Colors.category20().by(pv.index)); + +/** + * Returns the mid-radius of the wedge, which is defined as half-way between the + * inner and outer radii. + * + * @see #innerRadius + * @see #outerRadius + * @returns {number} the mid-radius, in pixels. + */ +pv.Wedge.prototype.midRadius = function() { + return (this.innerRadius() + this.outerRadius()) / 2; +}; + +/** + * Returns the mid-angle of the wedge, which is defined as half-way between the + * start and end angles. + * + * @see #startAngle + * @see #endAngle + * @returns {number} the mid-angle, in radians. + */ +pv.Wedge.prototype.midAngle = function() { + return (this.startAngle() + this.endAngle()) / 2; +}; + +/** + * Constructs a new wedge anchor with default properties. Wedges support five + * different anchors:<ul> + * + * <li>outer + * <li>inner + * <li>center + * <li>start + * <li>end + * + * </ul>In addition to positioning properties (left, right, top bottom), the + * anchors support text rendering properties (text-align, text-baseline, + * textAngle). Text is rendered to appear inside the wedge. + * + * @param {string} name the anchor name; either a string or a property function. + * @returns {pv.Anchor} + */ +pv.Wedge.prototype.anchor = function(name) { + function partial(s) { return s.innerRadius || s.angle < 2 * Math.PI; } + function midRadius(s) { return (s.innerRadius + s.outerRadius) / 2; } + function midAngle(s) { return (s.startAngle + s.endAngle) / 2; } + return pv.Mark.prototype.anchor.call(this, name) + .left(function() { + var s = this.scene.target[this.index]; + if (partial(s)) switch (this.name()) { + case "outer": return s.left + s.outerRadius * Math.cos(midAngle(s)); + case "inner": return s.left + s.innerRadius * Math.cos(midAngle(s)); + case "start": return s.left + midRadius(s) * Math.cos(s.startAngle); + case "center": return s.left + midRadius(s) * Math.cos(midAngle(s)); + case "end": return s.left + midRadius(s) * Math.cos(s.endAngle); + } + return s.left; + }) + .top(function() { + var s = this.scene.target[this.index]; + if (partial(s)) switch (this.name()) { + case "outer": return s.top + s.outerRadius * Math.sin(midAngle(s)); + case "inner": return s.top + s.innerRadius * Math.sin(midAngle(s)); + case "start": return s.top + midRadius(s) * Math.sin(s.startAngle); + case "center": return s.top + midRadius(s) * Math.sin(midAngle(s)); + case "end": return s.top + midRadius(s) * Math.sin(s.endAngle); + } + return s.top; + }) + .textAlign(function() { + var s = this.scene.target[this.index]; + if (partial(s)) switch (this.name()) { + case "outer": return pv.Wedge.upright(midAngle(s)) ? "right" : "left"; + case "inner": return pv.Wedge.upright(midAngle(s)) ? "left" : "right"; + } + return "center"; + }) + .textBaseline(function() { + var s = this.scene.target[this.index]; + if (partial(s)) switch (this.name()) { + case "start": return pv.Wedge.upright(s.startAngle) ? "top" : "bottom"; + case "end": return pv.Wedge.upright(s.endAngle) ? "bottom" : "top"; + } + return "middle"; + }) + .textAngle(function() { + var s = this.scene.target[this.index], a = 0; + if (partial(s)) switch (this.name()) { + case "center": + case "inner": + case "outer": a = midAngle(s); break; + case "start": a = s.startAngle; break; + case "end": a = s.endAngle; break; + } + return pv.Wedge.upright(a) ? a : (a + Math.PI); + }); +}; + +/** + * Returns true if the specified angle is considered "upright", as in, text + * rendered at that angle would appear upright. If the angle is not upright, + * text is rotated 180 degrees to be upright, and the text alignment properties + * are correspondingly changed. + * + * @param {number} angle an angle, in radius. + * @returns {boolean} true if the specified angle is upright. + */ +pv.Wedge.upright = function(angle) { + angle = angle % (2 * Math.PI); + angle = (angle < 0) ? (2 * Math.PI + angle) : angle; + return (angle < Math.PI / 2) || (angle >= 3 * Math.PI / 2); +}; + +/** @private Sets angle based on endAngle or vice versa. */ +pv.Wedge.prototype.buildImplied = function(s) { + if (s.angle == null) s.angle = s.endAngle - s.startAngle; + else if (s.endAngle == null) s.endAngle = s.startAngle + s.angle; + pv.Mark.prototype.buildImplied.call(this, s); +}; +/** + * Abstract; not implemented. There is no explicit constructor; this class + * merely serves to document the attributes that are used on particles in + * physics simulations. + * + * @class A weighted particle that can participate in a force simulation. + * + * @name pv.Particle + */ + +/** + * The next particle in the simulation. Particles form a singly-linked list. + * + * @field + * @type pv.Particle + * @name pv.Particle.prototype.next + */ + +/** + * The <i>x</i>-position of the particle. + * + * @field + * @type number + * @name pv.Particle.prototype.x + */ + +/** + * The <i>y</i>-position of the particle. + * + * @field + * @type number + * @name pv.Particle.prototype.y + */ + +/** + * The <i>x</i>-velocity of the particle. + * + * @field + * @type number + * @name pv.Particle.prototype.vx + */ + +/** + * The <i>y</i>-velocity of the particle. + * + * @field + * @type number + * @name pv.Particle.prototype.vy + */ + +/** + * The <i>x</i>-position of the particle at -dt. + * + * @field + * @type number + * @name pv.Particle.prototype.px + */ + +/** + * The <i>y</i>-position of the particle at -dt. + * + * @field + * @type number + * @name pv.Particle.prototype.py + */ + +/** + * The <i>x</i>-force on the particle. + * + * @field + * @type number + * @name pv.Particle.prototype.fx + */ + +/** + * The <i>y</i>-force on the particle. + * + * @field + * @type number + * @name pv.Particle.prototype.fy + */ +/** + * Constructs a new empty simulation. + * + * @param {array} particles + * @returns {pv.Simulation} a new simulation for the specified particles. + * @see pv.Simulation + */ +pv.simulation = function(particles) { + return new pv.Simulation(particles); +}; + +/** + * Constructs a new simulation for the specified particles. + * + * @class Represents a particle simulation. Particles are massive points in + * two-dimensional space. Forces can be applied to these particles, causing them + * to move. Constraints can also be applied to restrict particle movement, for + * example, constraining particles to a fixed position, or simulating collision + * between circular particles with area. + * + * <p>The simulation uses <a + * href="http://en.wikipedia.org/wiki/Verlet_integration">Position Verlet</a> + * integration, due to the ease with which <a + * href="http://www.teknikus.dk/tj/gdc2001.htm">geometric constraints</a> can be + * implemented. For each time step, Verlet integration is performed, new forces + * are accumulated, and then constraints are applied. + * + * <p>The simulation makes two simplifying assumptions: all particles are + * equal-mass, and the time step of the simulation is fixed. It would be easy to + * incorporate variable-mass particles as a future enhancement. Variable time + * steps are also possible, but are likely to introduce instability in the + * simulation. + * + * <p>This class can be used directly to simulate particle interaction. + * Alternatively, for network diagrams, see {@link pv.Layout.Force}. + * + * @param {array} particles an array of {@link pv.Particle}s to simulate. + * @see pv.Layout.Force + * @see pv.Force + * @see pv.Constraint + */ +pv.Simulation = function(particles) { + for (var i = 0; i < particles.length; i++) this.particle(particles[i]); +}; + +/** + * The particles in the simulation. Particles are stored as a linked list; this + * field represents the first particle in the simulation. + * + * @field + * @type pv.Particle + * @name pv.Simulation.prototype.particles + */ + +/** + * The forces in the simulation. Forces are stored as a linked list; this field + * represents the first force in the simulation. + * + * @field + * @type pv.Force + * @name pv.Simulation.prototype.forces + */ + +/** + * The constraints in the simulation. Constraints are stored as a linked list; + * this field represents the first constraint in the simulation. + * + * @field + * @type pv.Constraint + * @name pv.Simulation.prototype.constraints + */ + +/** + * Adds the specified particle to the simulation. + * + * @param {pv.Particle} p the new particle. + * @returns {pv.Simulation} this. + */ +pv.Simulation.prototype.particle = function(p) { + p.next = this.particles; + /* Default velocities and forces to zero if unset. */ + if (isNaN(p.px)) p.px = p.x; + if (isNaN(p.py)) p.py = p.y; + if (isNaN(p.fx)) p.fx = 0; + if (isNaN(p.fy)) p.fy = 0; + this.particles = p; + return this; +}; + +/** + * Adds the specified force to the simulation. + * + * @param {pv.Force} f the new force. + * @returns {pv.Simulation} this. + */ +pv.Simulation.prototype.force = function(f) { + f.next = this.forces; + this.forces = f; + return this; +}; + +/** + * Adds the specified constraint to the simulation. + * + * @param {pv.Constraint} c the new constraint. + * @returns {pv.Simulation} this. + */ +pv.Simulation.prototype.constraint = function(c) { + c.next = this.constraints; + this.constraints = c; + return this; +}; + +/** + * Apply constraints, and then set the velocities to zero. + * + * @returns {pv.Simulation} this. + */ +pv.Simulation.prototype.stabilize = function(n) { + var c; + if (!arguments.length) n = 3; // TODO use cooling schedule + for (var i = 0; i < n; i++) { + var q = new pv.Quadtree(this.particles); + for (c = this.constraints; c; c = c.next) c.apply(this.particles, q); + } + for (var p = this.particles; p; p = p.next) { + p.px = p.x; + p.py = p.y; + } + return this; +}; + +/** + * Advances the simulation one time-step. + */ +pv.Simulation.prototype.step = function() { + var p, f, c; + + /* + * Assumptions: + * - The mass (m) of every particles is 1. + * - The time step (dt) is 1. + */ + + /* Position Verlet integration. */ + for (p = this.particles; p; p = p.next) { + var px = p.px, py = p.py; + p.px = p.x; + p.py = p.y; + p.x += p.vx = ((p.x - px) + p.fx); + p.y += p.vy = ((p.y - py) + p.fy); + } + + /* Apply constraints, then accumulate new forces. */ + var q = new pv.Quadtree(this.particles); + for (c = this.constraints; c; c = c.next) c.apply(this.particles, q); + for (p = this.particles; p; p = p.next) p.fx = p.fy = 0; + for (f = this.forces; f; f = f.next) f.apply(this.particles, q); +}; +/** + * Constructs a new quadtree for the specified array of particles. + * + * @class Represents a quadtree: a two-dimensional recursive spatial + * subdivision. This particular implementation uses square partitions, dividing + * each square into four equally-sized squares. Each particle exists in a unique + * node; if multiple particles are in the same position, some particles may be + * stored on internal nodes rather than leaf nodes. + * + * <p>This quadtree can be used to accelerate various spatial operations, such + * as the Barnes-Hut approximation for computing n-body forces, or collision + * detection. + * + * @see pv.Force.charge + * @see pv.Constraint.collision + * @param {pv.Particle} particles the linked list of particles. + */ +pv.Quadtree = function(particles) { + var p; + + /* Compute bounds. */ + var x1 = Number.POSITIVE_INFINITY, y1 = x1, + x2 = Number.NEGATIVE_INFINITY, y2 = x2; + for (p = particles; p; p = p.next) { + if (p.x < x1) x1 = p.x; + if (p.y < y1) y1 = p.y; + if (p.x > x2) x2 = p.x; + if (p.y > y2) y2 = p.y; + } + + /* Squarify the bounds. */ + var dx = x2 - x1, dy = y2 - y1; + if (dx > dy) y2 = y1 + dx; + else x2 = x1 + dy; + this.xMin = x1; + this.yMin = y1; + this.xMax = x2; + this.yMax = y2; + + /** + * @ignore Recursively inserts the specified particle <i>p</i> at the node + * <i>n</i> or one of its descendants. The bounds are defined by [<i>x1</i>, + * <i>x2</i>] and [<i>y1</i>, <i>y2</i>]. + */ + function insert(n, p, x1, y1, x2, y2) { + if (isNaN(p.x) || isNaN(p.y)) return; // ignore invalid particles + if (n.leaf) { + if (n.p) { + /* + * If the particle at this leaf node is at the same position as the new + * particle we are adding, we leave the particle associated with the + * internal node while adding the new particle to a child node. This + * avoids infinite recursion. + */ + if ((Math.abs(n.p.x - p.x) + Math.abs(n.p.y - p.y)) < .01) { + insertChild(n, p, x1, y1, x2, y2); + } else { + var v = n.p; + n.p = null; + insertChild(n, v, x1, y1, x2, y2); + insertChild(n, p, x1, y1, x2, y2); + } + } else { + n.p = p; + } + } else { + insertChild(n, p, x1, y1, x2, y2); + } + } + + /** + * @ignore Recursively inserts the specified particle <i>p</i> into a + * descendant of node <i>n</i>. The bounds are defined by [<i>x1</i>, + * <i>x2</i>] and [<i>y1</i>, <i>y2</i>]. + */ + function insertChild(n, p, x1, y1, x2, y2) { + /* Compute the split point, and the quadrant in which to insert p. */ + var sx = (x1 + x2) * .5, + sy = (y1 + y2) * .5, + right = p.x >= sx, + bottom = p.y >= sy; + + /* Recursively insert into the child node. */ + n.leaf = false; + switch ((bottom << 1) + right) { + case 0: n = n.c1 || (n.c1 = new pv.Quadtree.Node()); break; + case 1: n = n.c2 || (n.c2 = new pv.Quadtree.Node()); break; + case 2: n = n.c3 || (n.c3 = new pv.Quadtree.Node()); break; + case 3: n = n.c4 || (n.c4 = new pv.Quadtree.Node()); break; + } + + /* Update the bounds as we recurse. */ + if (right) x1 = sx; else x2 = sx; + if (bottom) y1 = sy; else y2 = sy; + insert(n, p, x1, y1, x2, y2); + } + + /* Insert all particles. */ + this.root = new pv.Quadtree.Node(); + for (p = particles; p; p = p.next) insert(this.root, p, x1, y1, x2, y2); +}; + +/** + * The root node of the quadtree. + * + * @type pv.Quadtree.Node + * @name pv.Quadtree.prototype.root + */ + +/** + * The minimum x-coordinate value of all contained particles. + * + * @type number + * @name pv.Quadtree.prototype.xMin + */ + +/** + * The maximum x-coordinate value of all contained particles. + * + * @type number + * @name pv.Quadtree.prototype.xMax + */ + +/** + * The minimum y-coordinate value of all contained particles. + * + * @type number + * @name pv.Quadtree.prototype.yMin + */ + +/** + * The maximum y-coordinate value of all contained particles. + * + * @type number + * @name pv.Quadtree.prototype.yMax + */ + +/** + * Constructs a new node. + * + * @class A node in a quadtree. + * + * @see pv.Quadtree + */ +pv.Quadtree.Node = function() { + /* + * Prepopulating all attributes significantly increases performance! Also, + * letting the language interpreter manage garbage collection was moderately + * faster than creating a cache pool. + */ + this.leaf = true; + this.c1 = null; + this.c2 = null; + this.c3 = null; + this.c4 = null; + this.p = null; +}; + +/** + * True if this node is a leaf node; i.e., it has no children. Note that both + * leaf nodes and non-leaf (internal) nodes may have associated particles. If + * this is a non-leaf node, then at least one of {@link #c1}, {@link #c2}, + * {@link #c3} or {@link #c4} is guaranteed to be non-null. + * + * @type boolean + * @name pv.Quadtree.Node.prototype.leaf + */ + +/** + * The particle associated with this node, if any. + * + * @type pv.Particle + * @name pv.Quadtree.Node.prototype.p + */ + +/** + * The child node for the second quadrant, if any. + * + * @type pv.Quadtree.Node + * @name pv.Quadtree.Node.prototype.c2 + */ + +/** + * The child node for the third quadrant, if any. + * + * @type pv.Quadtree.Node + * @name pv.Quadtree.Node.prototype.c3 + */ + +/** + * The child node for the fourth quadrant, if any. + * + * @type pv.Quadtree.Node + * @name pv.Quadtree.Node.prototype.c4 + */ +/** + * Abstract; see an implementing class. + * + * @class Represents a force that acts on particles. Note that this interface + * does not specify how to bind a force to specific particles; in general, + * forces are applied globally to all particles. However, some forces may be + * applied to specific particles or between particles, such as spring forces, + * through additional specialization. + * + * @see pv.Simulation + * @see pv.Particle + * @see pv.Force.charge + * @see pv.Force.drag + * @see pv.Force.spring + */ +pv.Force = {}; + +/** + * Applies this force to the specified particles. + * + * @function + * @name pv.Force.prototype.apply + * @param {pv.Particle} particles particles to which to apply this force. + * @param {pv.Quadtree} q a quadtree for spatial acceleration. + */ +/** + * Constructs a new charge force, with an optional charge constant. The charge + * constant can be negative for repulsion (e.g., particles with electrical + * charge of equal sign), or positive for attraction (e.g., massive particles + * with mutual gravity). The default charge constant is -40. + * + * @class An n-body force, as defined by Coulomb's law or Newton's law of + * gravitation, inversely proportional to the square of the distance between + * particles. Note that the force is independent of the <i>mass</i> of the + * associated particles, and that the particles do not have charges of varying + * magnitude; instead, the attraction or repulsion of all particles is globally + * specified as the charge {@link #constant}. + * + * <p>This particular implementation uses the Barnes-Hut algorithm. For details, + * see <a + * href="http://www.nature.com/nature/journal/v324/n6096/abs/324446a0.html">"A + * hierarchical O(N log N) force-calculation algorithm"</a>, J. Barnes & + * P. Hut, <i>Nature</i> 1986. + * + * @name pv.Force.charge + * @param {number} [k] the charge constant. + */ +pv.Force.charge = function(k) { + var min = 2, // minimum distance at which to observe forces + min1 = 1 / min, + max = 500, // maximum distance at which to observe forces + max1 = 1 / max, + theta = .9, // Barnes-Hut theta approximation constant + force = {}; + + if (!arguments.length) k = -40; // default charge constant (repulsion) + + /** + * Sets or gets the charge constant. If an argument is specified, it is the + * new charge constant. The charge constant can be negative for repulsion + * (e.g., particles with electrical charge of equal sign), or positive for + * attraction (e.g., massive particles with mutual gravity). The default + * charge constant is -40. + * + * @function + * @name pv.Force.charge.prototype.constant + * @param {number} x the charge constant. + * @returns {pv.Force.charge} this. + */ + force.constant = function(x) { + if (arguments.length) { + k = Number(x); + return force; + } + return k; + }; + + /** + * Sets or gets the domain; specifies the minimum and maximum domain within + * which charge forces are applied. A minimum distance threshold avoids + * applying forces that are two strong (due to granularity of the simulation's + * numeric integration). A maximum distance threshold improves performance by + * skipping force calculations for particles that are far apart. + * + * <p>The default domain is [2, 500]. + * + * @function + * @name pv.Force.charge.prototype.domain + * @param {number} a + * @param {number} b + * @returns {pv.Force.charge} this. + */ + force.domain = function(a, b) { + if (arguments.length) { + min = Number(a); + min1 = 1 / min; + max = Number(b); + max1 = 1 / max; + return force; + } + return [min, max]; + }; + + /** + * Sets or gets the Barnes-Hut approximation factor. The Barnes-Hut + * approximation criterion is the ratio of the size of the quadtree node to + * the distance from the point to the node's center of mass is beneath some + * threshold. + * + * @function + * @name pv.Force.charge.prototype.theta + * @param {number} x the new Barnes-Hut approximation factor. + * @returns {pv.Force.charge} this. + */ + force.theta = function(x) { + if (arguments.length) { + theta = Number(x); + return force; + } + return theta; + }; + + /** + * @ignore Recursively computes the center of charge for each node in the + * quadtree. This is equivalent to the center of mass, assuming that all + * particles have unit weight. + */ + function accumulate(n) { + var cx = 0, cy = 0; + n.cn = 0; + function accumulateChild(c) { + accumulate(c); + n.cn += c.cn; + cx += c.cn * c.cx; + cy += c.cn * c.cy; + } + if (!n.leaf) { + if (n.c1) accumulateChild(n.c1); + if (n.c2) accumulateChild(n.c2); + if (n.c3) accumulateChild(n.c3); + if (n.c4) accumulateChild(n.c4); + } + if (n.p) { + n.cn += k; + cx += k * n.p.x; + cy += k * n.p.y; + } + n.cx = cx / n.cn; + n.cy = cy / n.cn; + } + + /** + * @ignore Recursively computes forces on the given particle using the given + * quadtree node. The Barnes-Hut approximation criterion is the ratio of the + * size of the quadtree node to the distance from the point to the node's + * center of mass is beneath some threshold. + */ + function forces(n, p, x1, y1, x2, y2) { + var dx = n.cx - p.x, + dy = n.cy - p.y, + dn = 1 / Math.sqrt(dx * dx + dy * dy); + + /* Barnes-Hut criterion. */ + if ((n.leaf && (n.p != p)) || ((x2 - x1) * dn < theta)) { + if (dn < max1) return; + if (dn > min1) dn = min1; + var kc = n.cn * dn * dn * dn, + fx = dx * kc, + fy = dy * kc; + p.fx += fx; + p.fy += fy; + } else if (!n.leaf) { + var sx = (x1 + x2) * .5, sy = (y1 + y2) * .5; + if (n.c1) forces(n.c1, p, x1, y1, sx, sy); + if (n.c2) forces(n.c2, p, sx, y1, x2, sy); + if (n.c3) forces(n.c3, p, x1, sy, sx, y2); + if (n.c4) forces(n.c4, p, sx, sy, x2, y2); + if (dn < max1) return; + if (dn > min1) dn = min1; + if (n.p && (n.p != p)) { + var kc = k * dn * dn * dn, + fx = dx * kc, + fy = dy * kc; + p.fx += fx; + p.fy += fy; + } + } + } + + /** + * Applies this force to the specified particles. The force is applied between + * all pairs of particles within the domain, using the specified quadtree to + * accelerate n-body force calculation using the Barnes-Hut approximation + * criterion. + * + * @function + * @name pv.Force.charge.prototype.apply + * @param {pv.Particle} particles particles to which to apply this force. + * @param {pv.Quadtree} q a quadtree for spatial acceleration. + */ + force.apply = function(particles, q) { + accumulate(q.root); + for (var p = particles; p; p = p.next) { + forces(q.root, p, q.xMin, q.yMin, q.xMax, q.yMax); + } + }; + + return force; +}; +/** + * Constructs a new drag force with the specified constant. + * + * @class Implements a drag force, simulating friction. The drag force is + * applied in the opposite direction of the particle's velocity. Since Position + * Verlet integration does not track velocities explicitly, the error term with + * this estimate of velocity is fairly high, so the drag force may be + * inaccurate. + * + * @extends pv.Force + * @param {number} k the drag constant. + * @see #constant + */ +pv.Force.drag = function(k) { + var force = {}; + + if (!arguments.length) k = .1; // default drag constant + + /** + * Sets or gets the drag constant, in the range [0,1]. The default drag + * constant is 0.1. The drag forces scales linearly with the particle's + * velocity based on the given drag constant. + * + * @function + * @name pv.Force.drag.prototype.constant + * @param {number} x the new drag constant. + * @returns {pv.Force.drag} this, or the current drag constant. + */ + force.constant = function(x) { + if (arguments.length) { k = x; return force; } + return k; + }; + + /** + * Applies this force to the specified particles. + * + * @function + * @name pv.Force.drag.prototype.apply + * @param {pv.Particle} particles particles to which to apply this force. + */ + force.apply = function(particles) { + if (k) for (var p = particles; p; p = p.next) { + p.fx -= k * p.vx; + p.fy -= k * p.vy; + } + }; + + return force; +}; +/** + * Constructs a new spring force with the specified constant. The links + * associated with this spring force must be specified before the spring force + * can be applied. + * + * @class Implements a spring force, per Hooke's law. The spring force can be + * configured with a tension constant, rest length, and damping factor. The + * tension and damping will automatically be normalized using the inverse square + * root of the maximum link degree of attached nodes; this makes springs weaker + * between nodes of high link degree. + * + * <p>Unlike other forces (such as charge and drag forces) which may be applied + * globally, spring forces are only applied between linked particles. Therefore, + * an array of links must be specified before this force can be applied; the + * links should be an array of {@link pv.Layout.Network.Link}s. See also + * {@link pv.Layout.Force} for an example of using spring and charge forces for + * network layout. + * + * @extends pv.Force + * @param {number} k the spring constant. + * @see #constant + * @see #links + */ +pv.Force.spring = function(k) { + var d = .1, // default damping factor + l = 20, // default rest length + links, // links on which to apply spring forces + kl, // per-spring normalization + force = {}; + + if (!arguments.length) k = .1; // default spring constant (tension) + + /** + * Sets or gets the links associated with this spring force. Unlike other + * forces (such as charge and drag forces) which may be applied globally, + * spring forces are only applied between linked particles. Therefore, an + * array of links must be specified before this force can be applied; the + * links should be an array of {@link pv.Layout.Network.Link}s. + * + * @function + * @name pv.Force.spring.prototype.links + * @param {array} x the new array of links. + * @returns {pv.Force.spring} this, or the current array of links. + */ + force.links = function(x) { + if (arguments.length) { + links = x; + kl = x.map(function(l) { + return 1 / Math.sqrt(Math.max( + l.sourceNode.linkDegree, + l.targetNode.linkDegree)); + }); + return force; + } + return links; + }; + + /** + * Sets or gets the spring constant. The default value is 0.1; greater values + * will result in stronger tension. The spring tension is automatically + * normalized using the inverse square root of the maximum link degree of + * attached nodes. + * + * @function + * @name pv.Force.spring.prototype.constant + * @param {number} x the new spring constant. + * @returns {pv.Force.spring} this, or the current spring constant. + */ + force.constant = function(x) { + if (arguments.length) { + k = Number(x); + return force; + } + return k; + }; + + /** + * The spring damping factor, in the range [0,1]. Damping functions + * identically to drag forces, damping spring bounciness by applying a force + * in the opposite direction of attached nodes' velocities. The default value + * is 0.1. The spring damping is automatically normalized using the inverse + * square root of the maximum link degree of attached nodes. + * + * @function + * @name pv.Force.spring.prototype.damping + * @param {number} x the new spring damping factor. + * @returns {pv.Force.spring} this, or the current spring damping factor. + */ + force.damping = function(x) { + if (arguments.length) { + d = Number(x); + return force; + } + return d; + }; + + /** + * The spring rest length. The default value is 20 pixels. + * + * @function + * @name pv.Force.spring.prototype.length + * @param {number} x the new spring rest length. + * @returns {pv.Force.spring} this, or the current spring rest length. + */ + force.length = function(x) { + if (arguments.length) { + l = Number(x); + return force; + } + return l; + }; + + /** + * Applies this force to the specified particles. + * + * @function + * @name pv.Force.spring.prototype.apply + * @param {pv.Particle} particles particles to which to apply this force. + */ + force.apply = function(particles) { + for (var i = 0; i < links.length; i++) { + var a = links[i].sourceNode, + b = links[i].targetNode, + dx = a.x - b.x, + dy = a.y - b.y, + dn = Math.sqrt(dx * dx + dy * dy), + dd = dn ? (1 / dn) : 1, + ks = k * kl[i], // normalized tension + kd = d * kl[i], // normalized damping + kk = (ks * (dn - l) + kd * (dx * (a.vx - b.vx) + dy * (a.vy - b.vy)) * dd) * dd, + fx = -kk * (dn ? dx : (.01 * (.5 - Math.random()))), + fy = -kk * (dn ? dy : (.01 * (.5 - Math.random()))); + a.fx += fx; + a.fy += fy; + b.fx -= fx; + b.fy -= fy; + } + }; + + return force; +}; +/** + * Abstract; see an implementing class. + * + * @class Represents a constraint that acts on particles. Note that this + * interface does not specify how to bind a constraint to specific particles; in + * general, constraints are applied globally to all particles. However, some + * constraints may be applied to specific particles or between particles, such + * as position constraints, through additional specialization. + * + * @see pv.Simulation + * @see pv.Particle + * @see pv.Constraint.bound + * @see pv.Constraint.collision + * @see pv.Constraint.position + */ +pv.Constraint = {}; + +/** + * Applies this constraint to the specified particles. + * + * @function + * @name pv.Constraint.prototype.apply + * @param {pv.Particle} particles particles to which to apply this constraint. + * @param {pv.Quadtree} q a quadtree for spatial acceleration. + * @returns {pv.Constraint} this. + */ +/** + * Constructs a new collision constraint. The default search radius is 10, and + * the default repeat count is 1. A radius function must be specified to compute + * the radius of particles. + * + * @class Constraints circles to avoid overlap. Each particle is treated as a + * circle, with the radius of the particle computed using a specified function. + * For example, if the particle has an <tt>r</tt> attribute storing the radius, + * the radius <tt>function(d) d.r</tt> specifies a collision constraint using + * this radius. The radius function is passed each {@link pv.Particle} as the + * first argument. + * + * <p>To accelerate collision detection, this implementation uses a quadtree and + * a search radius. The search radius is computed as the maximum radius of all + * particles in the simulation. + * + * @see pv.Constraint + * @param {function} radius the radius function. + */ +pv.Constraint.collision = function(radius) { + var n = 1, // number of times to repeat the constraint + r1, + px1, + py1, + px2, + py2, + constraint = {}; + + if (!arguments.length) r1 = 10; // default search radius + + /** + * Sets or gets the repeat count. If the repeat count is greater than 1, the + * constraint will be applied repeatedly; this is a form of the Gauss-Seidel + * method for constraints relaxation. Repeating the collision constraint makes + * the constraint have more of an effect when there is a potential for many + * co-occurring collisions. + * + * @function + * @name pv.Constraint.collision.prototype.repeat + * @param {number} x the number of times to repeat this constraint. + * @returns {pv.Constraint.collision} this. + */ + constraint.repeat = function(x) { + if (arguments.length) { + n = Number(x); + return constraint; + } + return n; + }; + + /** @private */ + function constrain(n, p, x1, y1, x2, y2) { + if (!n.leaf) { + var sx = (x1 + x2) * .5, + sy = (y1 + y2) * .5, + top = sy > py1, + bottom = sy < py2, + left = sx > px1, + right = sx < px2; + if (top) { + if (n.c1 && left) constrain(n.c1, p, x1, y1, sx, sy); + if (n.c2 && right) constrain(n.c2, p, sx, y1, x2, sy); + } + if (bottom) { + if (n.c3 && left) constrain(n.c3, p, x1, sy, sx, y2); + if (n.c4 && right) constrain(n.c4, p, sx, sy, x2, y2); + } + } + if (n.p && (n.p != p)) { + var dx = p.x - n.p.x, + dy = p.y - n.p.y, + l = Math.sqrt(dx * dx + dy * dy), + d = r1 + radius(n.p); + if (l < d) { + var k = (l - d) / l * .5; + dx *= k; + dy *= k; + p.x -= dx; + p.y -= dy; + n.p.x += dx; + n.p.y += dy; + } + } + } + + /** + * Applies this constraint to the specified particles. + * + * @function + * @name pv.Constraint.collision.prototype.apply + * @param {pv.Particle} particles particles to which to apply this constraint. + * @param {pv.Quadtree} q a quadtree for spatial acceleration. + */ + constraint.apply = function(particles, q) { + var p, r, max = -Infinity; + for (p = particles; p; p = p.next) { + r = radius(p); + if (r > max) max = r; + } + for (var i = 0; i < n; i++) { + for (p = particles; p; p = p.next) { + r = (r1 = radius(p)) + max; + px1 = p.x - r; + px2 = p.x + r; + py1 = p.y - r; + py2 = p.y + r; + constrain(q.root, p, q.xMin, q.yMin, q.xMax, q.yMax); + } + } + }; + + return constraint; +}; +/** + * Constructs a default position constraint using the <tt>fix</tt> attribute. + * An optional position function can be specified to determine how the fixed + * position per-particle is determined. + * + * @class Constraints particles to a fixed position. The fixed position per + * particle is determined using a given position function, which defaults to + * <tt>function(d) d.fix</tt>. + * + * <p>If the position function returns null, then no position constraint is + * applied to the given particle. Otherwise, the particle's position is set to + * the returned position, as expressed by a {@link pv.Vector}. (Note: the + * position does not need to be an instance of <tt>pv.Vector</tt>, but simply an + * object with <tt>x</tt> and <tt>y</tt> attributes.) + * + * <p>This constraint also supports a configurable alpha parameter, which + * defaults to 1. If the alpha parameter is in the range [0,1], then rather than + * setting the particle's new position directly to the position returned by the + * supplied position function, the particle's position is interpolated towards + * the fixed position. This results is a smooth (exponential) drift towards the + * fixed position, which can increase the stability of the physics simulation. + * In addition, the alpha parameter can be decayed over time, relaxing the + * position constraint, which helps to stabilize on an optimal solution. + * + * @param {function} [f] the position function. + */ +pv.Constraint.position = function(f) { + var a = 1, // default alpha + constraint = {}; + + if (!arguments.length) /** @ignore */ f = function(p) { return p.fix; }; + + /** + * Sets or gets the alpha parameter for position interpolation. If the alpha + * parameter is in the range [0,1], then rather than setting the particle's + * new position directly to the position returned by the supplied position + * function, the particle's position is interpolated towards the fixed + * position. + * + * @function + * @name pv.Constraint.position.prototype.alpha + * @param {number} x the new alpha parameter, in the range [0,1]. + * @returns {pv.Constraint.position} this. + */ + constraint.alpha = function(x) { + if (arguments.length) { + a = Number(x); + return constraint; + } + return a; + }; + + /** + * Applies this constraint to the specified particles. + * + * @function + * @name pv.Constraint.position.prototype.apply + * @param {pv.Particle} particles particles to which to apply this constraint. + */ + constraint.apply = function(particles) { + for (var p = particles; p; p = p.next) { + var v = f(p); + if (v) { + p.x += (v.x - p.x) * a; + p.y += (v.y - p.y) * a; + p.fx = p.fy = p.vx = p.vy = 0; + } + } + }; + + return constraint; +}; +/** + * Constructs a new bound constraint. Before the constraint can be used, the + * {@link #x} and {@link #y} methods must be call to specify the bounds. + * + * @class Constrains particles to within fixed rectangular bounds. For example, + * this constraint can be used to constrain particles in a physics simulation + * within the bounds of an enclosing panel. + * + * <p>Note that the current implementation treats particles as points, with no + * area. If the particles are rendered as dots, be sure to include some + * additional padding to inset the bounds such that the edges of the dots do not + * get clipped by the panel bounds. If the particles have different radii, this + * constraint would need to be extended using a radius function, similar to + * {@link pv.Constraint.collision}. + * + * @see pv.Layout.Force + * @extends pv.Constraint + */ +pv.Constraint.bound = function() { + var constraint = {}, + x, + y; + + /** + * Sets or gets the bounds on the x-coordinate. + * + * @function + * @name pv.Constraint.bound.prototype.x + * @param {number} min the minimum allowed x-coordinate. + * @param {number} max the maximum allowed x-coordinate. + * @returns {pv.Constraint.bound} this. + */ + constraint.x = function(min, max) { + if (arguments.length) { + x = {min: Math.min(min, max), max: Math.max(min, max)}; + return this; + } + return x; + }; + + /** + * Sets or gets the bounds on the y-coordinate. + * + * @function + * @name pv.Constraint.bound.prototype.y + * @param {number} min the minimum allowed y-coordinate. + * @param {number} max the maximum allowed y-coordinate. + * @returns {pv.Constraint.bound} this. + */ + constraint.y = function(min, max) { + if (arguments.length) { + y = {min: Math.min(min, max), max: Math.max(min, max)}; + return this; + } + return y; + }; + + /** + * Applies this constraint to the specified particles. + * + * @function + * @name pv.Constraint.bound.prototype.apply + * @param {pv.Particle} particles particles to which to apply this constraint. + */ + constraint.apply = function(particles) { + if (x) for (var p = particles; p; p = p.next) { + p.x = p.x < x.min ? x.min : (p.x > x.max ? x.max : p.x); + } + if (y) for (var p = particles; p; p = p.next) { + p.y = p.y < y.min ? y.min : (p.y > y.max ? y.max : p.y); + } + }; + + return constraint; +}; +/** + * Constructs a new, empty layout with default properties. Layouts are not + * typically constructed directly; instead, a concrete subclass is added to an + * existing panel via {@link pv.Mark#add}. + * + * @class Represents an abstract layout, encapsulating a visualization technique + * such as a streamgraph or treemap. Layouts are themselves containers, + * extending from {@link pv.Panel}, and defining a set of mark prototypes as + * children. These mark prototypes provide default properties that together + * implement the given visualization technique. + * + * <p>Layouts do not initially contain any marks; any exported marks (such as a + * network layout's <tt>link</tt> and <tt>node</tt>) are intended to be used as + * prototypes. By adding a concrete mark, such as a {@link pv.Bar}, to the + * appropriate mark prototype, the mark is added to the layout and inherits the + * given properties. This approach allows further customization of the layout, + * either by choosing a different mark type to add, or more simply by overriding + * some of the layout's defined properties. + * + * <p>Each concrete layout, such as treemap or circle-packing, has different + * behavior and may export different mark prototypes, depending on what marks + * are typically needed to render the desired visualization. Therefore it is + * important to understand how each layout is structured, such that the provided + * mark prototypes are used appropriately. + * + * <p>In addition to the mark prototypes, layouts may define custom properties + * that affect the overall behavior of the layout. For example, a treemap layout + * might use a property to specify which layout algorithm to use. These + * properties are just like other mark properties, and can be defined as + * constants or as functions. As with panels, the data property can be used to + * replicate layouts, and properties can be defined to in terms of layout data. + * + * @extends pv.Panel + */ +pv.Layout = function() { + pv.Panel.call(this); +}; + +pv.Layout.prototype = pv.extend(pv.Panel); + +/** + * @private Defines a local property with the specified name and cast. Note that + * although the property method is only defined locally, the cast function is + * global, which is necessary since properties are inherited! + * + * @param {string} name the property name. + * @param {function} [cast] the cast function for this property. + */ +pv.Layout.prototype.property = function(name, cast) { + if (!this.hasOwnProperty("properties")) { + this.properties = pv.extend(this.properties); + } + this.properties[name] = true; + this.propertyMethod(name, false, pv.Mark.cast[name] = cast); + return this; +}; +/** + * Constructs a new, empty network layout. Layouts are not typically constructed + * directly; instead, they are added to an existing panel via + * {@link pv.Mark#add}. + * + * @class Represents an abstract layout for network diagrams. This class + * provides the basic structure for both node-link diagrams (such as + * force-directed graph layout) and space-filling network diagrams (such as + * sunbursts and treemaps). Note that "network" here is a general term that + * includes hierarchical structures; a tree is represented using links from + * child to parent. + * + * <p>Network layouts require the graph data structure to be defined using two + * properties:<ul> + * + * <li><tt>nodes</tt> - an array of objects representing nodes. Objects in this + * array must conform to the {@link pv.Layout.Network.Node} interface; which is + * to say, be careful to avoid naming collisions with automatic attributes such + * as <tt>index</tt> and <tt>linkDegree</tt>. If the nodes property is defined + * as an array of primitives, such as numbers or strings, these primitives are + * automatically wrapped in an object; the resulting object's <tt>nodeValue</tt> + * attribute points to the original primitive value. + * + * <p><li><tt>links</tt> - an array of objects representing links. Objects in + * this array must conform to the {@link pv.Layout.Network.Link} interface; at a + * minimum, either <tt>source</tt> and <tt>target</tt> indexes or + * <tt>sourceNode</tt> and <tt>targetNode</tt> references must be set. Note that + * if the links property is defined after the nodes property, the links can be + * defined in terms of <tt>this.nodes()</tt>. + * + * </ul> + * + * <p>Three standard mark prototypes are provided:<ul> + * + * <li><tt>node</tt> - for rendering nodes; typically a {@link pv.Dot}. The node + * mark is added directly to the layout, with the data property defined via the + * layout's <tt>nodes</tt> property. Properties such as <tt>strokeStyle</tt> and + * <tt>fillStyle</tt> can be overridden to compute properties from node data + * dynamically. + * + * <p><li><tt>link</tt> - for rendering links; typically a {@link pv.Line}. The + * link mark is added to a child panel, whose data property is defined as + * layout's <tt>links</tt> property. The link's data property is then a + * two-element array of the source node and target node. Thus, poperties such as + * <tt>strokeStyle</tt> and <tt>fillStyle</tt> can be overridden to compute + * properties from either the node data (the first argument) or the link data + * (the second argument; the parent panel data) dynamically. + * + * <p><li><tt>label</tt> - for rendering node labels; typically a + * {@link pv.Label}. The label mark is added directly to the layout, with the + * data property defined via the layout's <tt>nodes</tt> property. Properties + * such as <tt>strokeStyle</tt> and <tt>fillStyle</tt> can be overridden to + * compute properties from node data dynamically. + * + * </ul>Note that some network implementations may not support all three + * standard mark prototypes; for example, space-filling hierarchical layouts + * typically do not use a <tt>link</tt> prototype, as the parent-child links are + * implied by the structure of the space-filling <tt>node</tt> marks. Check the + * specific network layout for implementation details. + * + * <p>Network layout properties, including <tt>nodes</tt> and <tt>links</tt>, + * are typically cached rather than re-evaluated with every call to render. This + * is a performance optimization, as network layout algorithms can be + * expensive. If the network structure changes, call {@link #reset} to clear the + * cache before rendering. Note that although the network layout properties are + * cached, child mark properties, such as the marks used to render the nodes and + * links, <i>are not</i>. Therefore, non-structural changes to the network + * layout, such as changing the color of a mark on mouseover, do not need to + * reset the layout. + * + * @see pv.Layout.Hierarchy + * @see pv.Layout.Force + * @see pv.Layout.Matrix + * @see pv.Layout.Arc + * @see pv.Layout.Rollup + * @extends pv.Layout + */ +pv.Layout.Network = function() { + pv.Layout.call(this); + var that = this; + + /* @private Version tracking to cache layout state, improving performance. */ + this.$id = pv.id(); + + /** + * The node prototype. This prototype is intended to be used with a Dot mark + * in conjunction with the link prototype. + * + * @type pv.Mark + * @name pv.Layout.Network.prototype.node + */ + (this.node = new pv.Mark() + .data(function() { return that.nodes(); }) + .strokeStyle("#1f77b4") + .fillStyle("#fff") + .left(function(n) { return n.x; }) + .top(function(n) { return n.y; })).parent = this; + + /** + * The link prototype, which renders edges between source nodes and target + * nodes. This prototype is intended to be used with a Line mark in + * conjunction with the node prototype. + * + * @type pv.Mark + * @name pv.Layout.Network.prototype.link + */ + this.link = new pv.Mark() + .extend(this.node) + .data(function(p) { return [p.sourceNode, p.targetNode]; }) + .fillStyle(null) + .lineWidth(function(d, p) { return p.linkValue * 1.5; }) + .strokeStyle("rgba(0,0,0,.2)"); + + this.link.add = function(type) { + return that.add(pv.Panel) + .data(function() { return that.links(); }) + .add(type) + .extend(this); + }; + + /** + * The node label prototype, which renders the node name adjacent to the node. + * This prototype is provided as an alternative to using the anchor on the + * node mark; it is primarily intended to be used with radial node-link + * layouts, since it provides a convenient mechanism to set the text angle. + * + * @type pv.Mark + * @name pv.Layout.Network.prototype.label + */ + (this.label = new pv.Mark() + .extend(this.node) + .textMargin(7) + .textBaseline("middle") + .text(function(n) { return n.nodeName || n.nodeValue; }) + .textAngle(function(n) { + var a = n.midAngle; + return pv.Wedge.upright(a) ? a : (a + Math.PI); + }) + .textAlign(function(n) { + return pv.Wedge.upright(n.midAngle) ? "left" : "right"; + })).parent = this; +}; + +/** + * @class Represents a node in a network layout. There is no explicit + * constructor; this class merely serves to document the attributes that are + * used on nodes in network layouts. (Note that hierarchical nodes place + * additional requirements on node representation, vis {@link pv.Dom.Node}.) + * + * @see pv.Layout.Network + * @name pv.Layout.Network.Node + */ + +/** + * The node index, zero-based. This attribute is populated automatically based + * on the index in the array returned by the <tt>nodes</tt> property. + * + * @type number + * @name pv.Layout.Network.Node.prototype.index + */ + +/** + * The link degree; the sum of link values for all incoming and outgoing links. + * This attribute is populated automatically. + * + * @type number + * @name pv.Layout.Network.Node.prototype.linkDegree + */ + +/** + * The node name; optional. If present, this attribute will be used to provide + * the text for node labels. If not present, the label text will fallback to the + * <tt>nodeValue</tt> attribute. + * + * @type string + * @name pv.Layout.Network.Node.prototype.nodeName + */ + +/** + * The node value; optional. If present, and no <tt>nodeName</tt> attribute is + * present, the node value will be used as the label text. This attribute is + * also automatically populated if the nodes are specified as an array of + * primitives, such as strings or numbers. + * + * @type object + * @name pv.Layout.Network.Node.prototype.nodeValue + */ + +/** + * @class Represents a link in a network layout. There is no explicit + * constructor; this class merely serves to document the attributes that are + * used on links in network layouts. For hierarchical layouts, this class is + * used to represent the parent-child links. + * + * @see pv.Layout.Network + * @name pv.Layout.Network.Link + */ + +/** + * The link value, or weight; optional. If not specified (or not a number), the + * default value of 1 is used. + * + * @type number + * @name pv.Layout.Network.Link.prototype.linkValue + */ + +/** + * The link's source node. If not set, this value will be derived from the + * <tt>source</tt> attribute index. + * + * @type pv.Layout.Network.Node + * @name pv.Layout.Network.Link.prototype.sourceNode + */ + +/** + * The link's target node. If not set, this value will be derived from the + * <tt>target</tt> attribute index. + * + * @type pv.Layout.Network.Node + * @name pv.Layout.Network.Link.prototype.targetNode + */ + +/** + * Alias for <tt>sourceNode</tt>, as expressed by the index of the source node. + * This attribute is not populated automatically, but may be used as a more + * convenient identification of the link's source, for example in a static JSON + * representation. + * + * @type number + * @name pv.Layout.Network.Link.prototype.source + */ + +/** + * Alias for <tt>targetNode</tt>, as expressed by the index of the target node. + * This attribute is not populated automatically, but may be used as a more + * convenient identification of the link's target, for example in a static JSON + * representation. + * + * @type number + * @name pv.Layout.Network.Link.prototype.target + */ + +/** + * Alias for <tt>linkValue</tt>. This attribute is not populated automatically, + * but may be used instead of the <tt>linkValue</tt> attribute when specifying + * links. + * + * @type number + * @name pv.Layout.Network.Link.prototype.value + */ + +/** @private Transform nodes and links on cast. */ +pv.Layout.Network.prototype = pv.extend(pv.Layout) + .property("nodes", function(v) { + return v.map(function(d, i) { + if (typeof d != "object") d = {nodeValue: d}; + d.index = i; + return d; + }); + }) + .property("links", function(v) { + return v.map(function(d) { + if (isNaN(d.linkValue)) d.linkValue = isNaN(d.value) ? 1 : d.value; + return d; + }); + }); + +/** + * Resets the cache, such that changes to layout property definitions will be + * visible on subsequent render. Unlike normal marks (and normal layouts), + * properties associated with network layouts are not automatically re-evaluated + * on render; the properties are cached, and any expensive layout algorithms are + * only run after the layout is explicitly reset. + * + * @returns {pv.Layout.Network} this. + */ +pv.Layout.Network.prototype.reset = function() { + this.$id = pv.id(); + return this; +}; + +/** @private Skip evaluating properties if cached. */ +pv.Layout.Network.prototype.buildProperties = function(s, properties) { + if ((s.$id || 0) < this.$id) { + pv.Layout.prototype.buildProperties.call(this, s, properties); + } +}; + +/** @private Compute link degrees; map source and target indexes to nodes. */ +pv.Layout.Network.prototype.buildImplied = function(s) { + pv.Layout.prototype.buildImplied.call(this, s); + if (s.$id >= this.$id) return true; + s.$id = this.$id; + s.nodes.forEach(function(d) { + d.linkDegree = 0; + }); + s.links.forEach(function(d) { + var v = d.linkValue; + (d.sourceNode || (d.sourceNode = s.nodes[d.source])).linkDegree += v; + (d.targetNode || (d.targetNode = s.nodes[d.target])).linkDegree += v; + }); +}; +/** + * Constructs a new, empty hierarchy layout. Layouts are not typically + * constructed directly; instead, they are added to an existing panel via + * {@link pv.Mark#add}. + * + * @class Represents an abstract layout for hierarchy diagrams. This class is a + * specialization of {@link pv.Layout.Network}, providing the basic structure + * for both hierarchical node-link diagrams (such as Reingold-Tilford trees) and + * space-filling hierarchy diagrams (such as sunbursts and treemaps). + * + * <p>Unlike general network layouts, the <tt>links</tt> property need not be + * defined explicitly. Instead, the links are computed implicitly from the + * <tt>parentNode</tt> attribute of the node objects, as defined by the + * <tt>nodes</tt> property. This implementation is also available as + * {@link #links}, for reuse with non-hierarchical layouts; for example, to + * render a tree using force-directed layout. + * + * <p>Correspondingly, the <tt>nodes</tt> property is represented as a union of + * {@link pv.Layout.Network.Node} and {@link pv.Dom.Node}. To construct a node + * hierarchy from a simple JSON map, use the {@link pv.Dom} operator; this + * operator also provides an easy way to sort nodes before passing them to the + * layout. + * + * <p>For more details on how to use this layout, see + * {@link pv.Layout.Network}. + * + * @see pv.Layout.Cluster + * @see pv.Layout.Partition + * @see pv.Layout.Tree + * @see pv.Layout.Treemap + * @see pv.Layout.Indent + * @see pv.Layout.Pack + * @extends pv.Layout.Network + */ +pv.Layout.Hierarchy = function() { + pv.Layout.Network.call(this); + this.link.strokeStyle("#ccc"); +}; + +pv.Layout.Hierarchy.prototype = pv.extend(pv.Layout.Network); + +/** @private Compute the implied links. (Links are null by default.) */ +pv.Layout.Hierarchy.prototype.buildImplied = function(s) { + if (!s.links) s.links = pv.Layout.Hierarchy.links.call(this); + pv.Layout.Network.prototype.buildImplied.call(this, s); +}; + +/** The implied links; computes links using the <tt>parentNode</tt> attribute. */ +pv.Layout.Hierarchy.links = function() { + return this.nodes() + .filter(function(n) { return n.parentNode; }) + .map(function(n) { + return { + sourceNode: n, + targetNode: n.parentNode, + linkValue: 1 + }; + }); +}; + +/** @private Provides standard node-link layout based on breadth & depth. */ +pv.Layout.Hierarchy.NodeLink = { + + /** @private */ + buildImplied: function(s) { + var nodes = s.nodes, + orient = s.orient, + horizontal = /^(top|bottom)$/.test(orient), + w = s.width, + h = s.height; + + /* Compute default inner and outer radius. */ + if (orient == "radial") { + var ir = s.innerRadius, or = s.outerRadius; + if (ir == null) ir = 0; + if (or == null) or = Math.min(w, h) / 2; + } + + /** @private Returns the radius of the given node. */ + function radius(n) { + return n.parentNode ? (n.depth * (or - ir) + ir) : 0; + } + + /** @private Returns the angle of the given node. */ + function midAngle(n) { + return (n.parentNode ? (n.breadth - .25) * 2 * Math.PI : 0); + } + + /** @private */ + function x(n) { + switch (orient) { + case "left": return n.depth * w; + case "right": return w - n.depth * w; + case "top": return n.breadth * w; + case "bottom": return w - n.breadth * w; + case "radial": return w / 2 + radius(n) * Math.cos(n.midAngle); + } + } + + /** @private */ + function y(n) { + switch (orient) { + case "left": return n.breadth * h; + case "right": return h - n.breadth * h; + case "top": return n.depth * h; + case "bottom": return h - n.depth * h; + case "radial": return h / 2 + radius(n) * Math.sin(n.midAngle); + } + } + + for (var i = 0; i < nodes.length; i++) { + var n = nodes[i]; + n.midAngle = orient == "radial" ? midAngle(n) + : horizontal ? Math.PI / 2 : 0; + n.x = x(n); + n.y = y(n); + if (n.firstChild) n.midAngle += Math.PI; + } + } +}; + +/** @private Provides standard space-filling layout based on breadth & depth. */ +pv.Layout.Hierarchy.Fill = { + + /** @private */ + constructor: function() { + this.node + .strokeStyle("#fff") + .fillStyle("#ccc") + .width(function(n) { return n.dx; }) + .height(function(n) { return n.dy; }) + .innerRadius(function(n) { return n.innerRadius; }) + .outerRadius(function(n) { return n.outerRadius; }) + .startAngle(function(n) { return n.startAngle; }) + .angle(function(n) { return n.angle; }); + + this.label + .textAlign("center") + .left(function(n) { return n.x + (n.dx / 2); }) + .top(function(n) { return n.y + (n.dy / 2); }); + + /* Hide unsupported link. */ + delete this.link; + }, + + /** @private */ + buildImplied: function(s) { + var nodes = s.nodes, + orient = s.orient, + horizontal = /^(top|bottom)$/.test(orient), + w = s.width, + h = s.height, + depth = -nodes[0].minDepth; + + /* Compute default inner and outer radius. */ + if (orient == "radial") { + var ir = s.innerRadius, or = s.outerRadius; + if (ir == null) ir = 0; + if (ir) depth *= 2; // use full depth step for root + if (or == null) or = Math.min(w, h) / 2; + } + + /** @private Scales the specified depth for a space-filling layout. */ + function scale(d, depth) { + return (d + depth) / (1 + depth); + } + + /** @private */ + function x(n) { + switch (orient) { + case "left": return scale(n.minDepth, depth) * w; + case "right": return (1 - scale(n.maxDepth, depth)) * w; + case "top": return n.minBreadth * w; + case "bottom": return (1 - n.maxBreadth) * w; + case "radial": return w / 2; + } + } + + /** @private */ + function y(n) { + switch (orient) { + case "left": return n.minBreadth * h; + case "right": return (1 - n.maxBreadth) * h; + case "top": return scale(n.minDepth, depth) * h; + case "bottom": return (1 - scale(n.maxDepth, depth)) * h; + case "radial": return h / 2; + } + } + + /** @private */ + function dx(n) { + switch (orient) { + case "left": + case "right": return (n.maxDepth - n.minDepth) / (1 + depth) * w; + case "top": + case "bottom": return (n.maxBreadth - n.minBreadth) * w; + case "radial": return n.parentNode ? (n.innerRadius + n.outerRadius) * Math.cos(n.midAngle) : 0; + } + } + + /** @private */ + function dy(n) { + switch (orient) { + case "left": + case "right": return (n.maxBreadth - n.minBreadth) * h; + case "top": + case "bottom": return (n.maxDepth - n.minDepth) / (1 + depth) * h; + case "radial": return n.parentNode ? (n.innerRadius + n.outerRadius) * Math.sin(n.midAngle) : 0; + } + } + + /** @private */ + function innerRadius(n) { + return Math.max(0, scale(n.minDepth, depth / 2)) * (or - ir) + ir; + } + + /** @private */ + function outerRadius(n) { + return scale(n.maxDepth, depth / 2) * (or - ir) + ir; + } + + /** @private */ + function startAngle(n) { + return (n.parentNode ? n.minBreadth - .25 : 0) * 2 * Math.PI; + } + + /** @private */ + function angle(n) { + return (n.parentNode ? n.maxBreadth - n.minBreadth : 1) * 2 * Math.PI; + } + + for (var i = 0; i < nodes.length; i++) { + var n = nodes[i]; + n.x = x(n); + n.y = y(n); + if (orient == "radial") { + n.innerRadius = innerRadius(n); + n.outerRadius = outerRadius(n); + n.startAngle = startAngle(n); + n.angle = angle(n); + n.midAngle = n.startAngle + n.angle / 2; + } else { + n.midAngle = horizontal ? -Math.PI / 2 : 0; + } + n.dx = dx(n); + n.dy = dy(n); + } + } +}; +/** + * Constructs a new, empty grid layout. Layouts are not typically constructed + * directly; instead, they are added to an existing panel via + * {@link pv.Mark#add}. + * + * @class Implements a grid layout with regularly-sized rows and columns. The + * number of rows and columns are determined from their respective + * properties. For example, the 2×3 array: + * + * <pre>1 2 3 + * 4 5 6</pre> + * + * can be represented using the <tt>rows</tt> property as: + * + * <pre>[[1, 2, 3], [4, 5, 6]]</pre> + * + * If your data is in column-major order, you can equivalently use the + * <tt>columns</tt> property. If the <tt>rows</tt> property is an array, it + * takes priority over the <tt>columns</tt> property. The data is implicitly + * transposed, as if the {@link pv.transpose} operator were applied. + * + * <p>This layout exports a single <tt>cell</tt> mark prototype, which is + * intended to be used with a bar, panel, layout, or subclass thereof. The data + * property of the cell prototype is defined as the elements in the array. For + * example, if the array is a two-dimensional array of values in the range + * [0,1], a simple heatmap can be generated as: + * + * <pre>vis.add(pv.Layout.Grid) + * .rows(arrays) + * .cell.add(pv.Bar) + * .fillStyle(pv.ramp("white", "black"))</pre> + * + * The grid subdivides the full width and height of the parent panel into equal + * rectangles. Note, however, that for large, interactive, or animated heatmaps, + * you may see significantly better performance through dynamic {@link pv.Image} + * generation. + * + * <p>For irregular grids using value-based spatial partitioning, see {@link + * pv.Layout.Treemap}. + * + * @extends pv.Layout + */ +pv.Layout.Grid = function() { + pv.Layout.call(this); + var that = this; + + /** + * The cell prototype. This prototype is intended to be used with a bar, + * panel, or layout (or subclass thereof) to render the grid cells. + * + * @type pv.Mark + * @name pv.Layout.Grid.prototype.cell + */ + (this.cell = new pv.Mark() + .data(function() { + return that.scene[that.index].$grid; + }) + .width(function() { + return that.width() / that.cols(); + }) + .height(function() { + return that.height() / that.rows(); + }) + .left(function() { + return this.width() * (this.index % that.cols()); + }) + .top(function() { + return this.height() * Math.floor(this.index / that.cols()); + })).parent = this; +}; + +pv.Layout.Grid.prototype = pv.extend(pv.Layout) + .property("rows") + .property("cols"); + +/** + * Default properties for grid layouts. By default, there is one row and one + * column, and the data is the propagated to the child cell. + * + * @type pv.Layout.Grid + */ +pv.Layout.Grid.prototype.defaults = new pv.Layout.Grid() + .extend(pv.Layout.prototype.defaults) + .rows(1) + .cols(1); + +/** @private */ +pv.Layout.Grid.prototype.buildImplied = function(s) { + pv.Layout.prototype.buildImplied.call(this, s); + var r = s.rows, c = s.cols; + if (typeof c == "object") r = pv.transpose(c); + if (typeof r == "object") { + s.$grid = pv.blend(r); + s.rows = r.length; + s.cols = r[0] ? r[0].length : 0; + } else { + s.$grid = pv.repeat([s.data], r * c); + } +}; + +/** + * The number of rows. This property can also be specified as the data in + * row-major order; in this case, the rows property is implicitly set to the + * length of the array, and the cols property is set to the length of the first + * element in the array. + * + * @type number + * @name pv.Layout.Grid.prototype.rows + */ + +/** + * The number of columns. This property can also be specified as the data in + * column-major order; in this case, the cols property is implicitly set to the + * length of the array, and the rows property is set to the length of the first + * element in the array. + * + * @type number + * @name pv.Layout.Grid.prototype.cols + */ +/** + * Constructs a new, empty stack layout. Layouts are not typically constructed + * directly; instead, they are added to an existing panel via + * {@link pv.Mark#add}. + * + * @class Implements a layout for stacked visualizations, ranging from simple + * stacked bar charts to more elaborate "streamgraphs" composed of stacked + * areas. Stack layouts uses length as a visual encoding, as opposed to + * position, as the layers do not share an aligned axis. + * + * <p>Marks can be stacked vertically or horizontally. For example, + * + * <pre>vis.add(pv.Layout.Stack) + * .layers([[1, 1.2, 1.7, 1.5, 1.7], + * [.5, 1, .8, 1.1, 1.3], + * [.2, .5, .8, .9, 1]]) + * .x(function() this.index * 35) + * .y(function(d) d * 40) + * .layer.add(pv.Area);</pre> + * + * specifies a vertically-stacked area chart, using the default "bottom-left" + * orientation with "zero" offset. This visualization can be easily changed into + * a streamgraph using the "wiggle" offset, which attempts to minimize change in + * slope weighted by layer thickness. See the {@link #offset} property for more + * supported streamgraph algorithms. + * + * <p>In the simplest case, the layer data can be specified as a two-dimensional + * array of numbers. The <tt>x</tt> and <tt>y</tt> psuedo-properties are used to + * define the thickness of each layer at the given position, respectively; in + * the above example of the "bottom-left" orientation, the <tt>x</tt> and + * <tt>y</tt> psuedo-properties are equivalent to the <tt>left</tt> and + * <tt>height</tt> properties that you might use if you implemented a stacked + * area by hand. + * + * <p>The advantage of using the stack layout is that the baseline, i.e., the + * <tt>bottom</tt> property is computed automatically using the specified offset + * algorithm. In addition, the order of layers can be computed using a built-in + * algorithm via the <tt>order</tt> property. + * + * <p>With the exception of the "expand" <tt>offset</tt>, the stack layout does + * not perform any automatic scaling of data; the values returned from + * <tt>x</tt> and <tt>y</tt> specify pixel sizes. To simplify scaling math, use + * this layout in conjunction with {@link pv.Scale.linear} or similar. + * + * <p>In other cases, the <tt>values</tt> psuedo-property can be used to define + * the data more flexibly. As with a typical panel & area, the + * <tt>layers</tt> property corresponds to the data in the enclosing panel, + * while the <tt>values</tt> psuedo-property corresponds to the data for the + * area within the panel. For example, given an array of data values: + * + * <pre>var crimea = [ + * { date: "4/1854", wounds: 0, other: 110, disease: 110 }, + * { date: "5/1854", wounds: 0, other: 95, disease: 105 }, + * { date: "6/1854", wounds: 0, other: 40, disease: 95 }, + * ...</pre> + * + * and a corresponding array of series names: + * + * <pre>var causes = ["wounds", "other", "disease"];</pre> + * + * Separate layers can be defined for each cause like so: + * + * <pre>vis.add(pv.Layout.Stack) + * .layers(causes) + * .values(crimea) + * .x(function(d) x(d.date)) + * .y(function(d, p) y(d[p])) + * .layer.add(pv.Area) + * ...</pre> + * + * As with the panel & area case, the datum that is passed to the + * psuedo-properties <tt>x</tt> and <tt>y</tt> are the values (an element in + * <tt>crimea</tt>); the second argument is the layer data (a string in + * <tt>causes</tt>). Additional arguments specify the data of enclosing panels, + * if any. + * + * @extends pv.Layout + */ +pv.Layout.Stack = function() { + pv.Layout.call(this); + var that = this, + /** @ignore */ none = function() { return null; }, + prop = {t: none, l: none, r: none, b: none, w: none, h: none}, + values, + buildImplied = that.buildImplied; + + /** @private Proxy the given property on the layer. */ + function proxy(name) { + return function() { + return prop[name](this.parent.index, this.index); + }; + } + + /** @private Compute the layout! */ + this.buildImplied = function(s) { + buildImplied.call(this, s); + + var data = s.layers, + n = data.length, + m, + orient = s.orient, + horizontal = /^(top|bottom)\b/.test(orient), + h = this.parent[horizontal ? "height" : "width"](), + x = [], + y = [], + dy = []; + + /* + * Iterate over the data, evaluating the values, x and y functions. The + * context in which the x and y psuedo-properties are evaluated is a + * pseudo-mark that is a grandchild of this layout. + */ + var stack = pv.Mark.stack, o = {parent: {parent: this}}; + stack.unshift(null); + values = []; + for (var i = 0; i < n; i++) { + dy[i] = []; + y[i] = []; + o.parent.index = i; + stack[0] = data[i]; + values[i] = this.$values.apply(o.parent, stack); + if (!i) m = values[i].length; + stack.unshift(null); + for (var j = 0; j < m; j++) { + stack[0] = values[i][j]; + o.index = j; + if (!i) x[j] = this.$x.apply(o, stack); + dy[i][j] = this.$y.apply(o, stack); + } + stack.shift(); + } + stack.shift(); + + /* order */ + var index; + switch (s.order) { + case "inside-out": { + var max = dy.map(function(v) { return pv.max.index(v); }), + map = pv.range(n).sort(function(a, b) { return max[a] - max[b]; }), + sums = dy.map(function(v) { return pv.sum(v); }), + top = 0, + bottom = 0, + tops = [], + bottoms = []; + for (var i = 0; i < n; i++) { + var j = map[i]; + if (top < bottom) { + top += sums[j]; + tops.push(j); + } else { + bottom += sums[j]; + bottoms.push(j); + } + } + index = bottoms.reverse().concat(tops); + break; + } + case "reverse": index = pv.range(n - 1, -1, -1); break; + default: index = pv.range(n); break; + } + + /* offset */ + switch (s.offset) { + case "silohouette": { + for (var j = 0; j < m; j++) { + var o = 0; + for (var i = 0; i < n; i++) o += dy[i][j]; + y[index[0]][j] = (h - o) / 2; + } + break; + } + case "wiggle": { + var o = 0; + for (var i = 0; i < n; i++) o += dy[i][0]; + y[index[0]][0] = o = (h - o) / 2; + for (var j = 1; j < m; j++) { + var s1 = 0, s2 = 0, dx = x[j] - x[j - 1]; + for (var i = 0; i < n; i++) s1 += dy[i][j]; + for (var i = 0; i < n; i++) { + var s3 = (dy[index[i]][j] - dy[index[i]][j - 1]) / (2 * dx); + for (var k = 0; k < i; k++) { + s3 += (dy[index[k]][j] - dy[index[k]][j - 1]) / dx; + } + s2 += s3 * dy[index[i]][j]; + } + y[index[0]][j] = o -= s1 ? s2 / s1 * dx : 0; + } + break; + } + case "expand": { + for (var j = 0; j < m; j++) { + y[index[0]][j] = 0; + var k = 0; + for (var i = 0; i < n; i++) k += dy[i][j]; + if (k) { + k = h / k; + for (var i = 0; i < n; i++) dy[i][j] *= k; + } else { + k = h / n; + for (var i = 0; i < n; i++) dy[i][j] = k; + } + } + break; + } + default: { + for (var j = 0; j < m; j++) y[index[0]][j] = 0; + break; + } + } + + /* Propagate the offset to the other series. */ + for (var j = 0; j < m; j++) { + var o = y[index[0]][j]; + for (var i = 1; i < n; i++) { + o += dy[index[i - 1]][j]; + y[index[i]][j] = o; + } + } + + /* Find the property definitions for dynamic substitution. */ + var i = orient.indexOf("-"), + pdy = horizontal ? "h" : "w", + px = i < 0 ? (horizontal ? "l" : "b") : orient.charAt(i + 1), + py = orient.charAt(0); + for (var p in prop) prop[p] = none; + prop[px] = function(i, j) { return x[j]; }; + prop[py] = function(i, j) { return y[i][j]; }; + prop[pdy] = function(i, j) { return dy[i][j]; }; + }; + + /** + * The layer prototype. This prototype is intended to be used with an area, + * bar or panel mark (or subclass thereof). Other mark types may be possible, + * though note that the stack layout is not currently designed to support + * radial stacked visualizations using wedges. + * + * <p>The layer is not a direct child of the stack layout; a hidden panel is + * used to replicate layers. + * + * @type pv.Mark + * @name pv.Layout.Stack.prototype.layer + */ + this.layer = new pv.Mark() + .data(function() { return values[this.parent.index]; }) + .top(proxy("t")) + .left(proxy("l")) + .right(proxy("r")) + .bottom(proxy("b")) + .width(proxy("w")) + .height(proxy("h")); + + this.layer.add = function(type) { + return that.add(pv.Panel) + .data(function() { return that.layers(); }) + .add(type) + .extend(this); + }; +}; + +pv.Layout.Stack.prototype = pv.extend(pv.Layout) + .property("orient", String) + .property("offset", String) + .property("order", String) + .property("layers"); + +/** + * Default properties for stack layouts. The default orientation is + * "bottom-left", the default offset is "zero", and the default layers is + * <tt>[[]]</tt>. + * + * @type pv.Layout.Stack + */ +pv.Layout.Stack.prototype.defaults = new pv.Layout.Stack() + .extend(pv.Layout.prototype.defaults) + .orient("bottom-left") + .offset("zero") + .layers([[]]); + +/** @private */ +pv.Layout.Stack.prototype.$x + = /** @private */ pv.Layout.Stack.prototype.$y + = function() { return 0; }; + +/** + * The x psuedo-property; determines the position of the value within the layer. + * This typically corresponds to the independent variable. For example, with the + * default "bottom-left" orientation, this function defines the "left" property. + * + * @param {function} f the x function. + * @returns {pv.Layout.Stack} this. + */ +pv.Layout.Stack.prototype.x = function(f) { + /** @private */ this.$x = pv.functor(f); + return this; +}; + +/** + * The y psuedo-property; determines the thickness of the layer at the given + * value. This typically corresponds to the dependent variable. For example, + * with the default "bottom-left" orientation, this function defines the + * "height" property. + * + * @param {function} f the y function. + * @returns {pv.Layout.Stack} this. + */ +pv.Layout.Stack.prototype.y = function(f) { + /** @private */ this.$y = pv.functor(f); + return this; +}; + +/** @private The default value function; identity. */ +pv.Layout.Stack.prototype.$values = pv.identity; + +/** + * The values function; determines the values for a given layer. The default + * value is the identity function, which assumes that the layers property is + * specified as a two-dimensional (i.e., nested) array. + * + * @param {function} f the values function. + * @returns {pv.Layout.Stack} this. + */ +pv.Layout.Stack.prototype.values = function(f) { + this.$values = pv.functor(f); + return this; +}; + +/** + * The layer data in row-major order. The value of this property is typically a + * two-dimensional (i.e., nested) array, but any array can be used, provided the + * values psuedo-property is defined accordingly. + * + * @type array[] + * @name pv.Layout.Stack.prototype.layers + */ + +/** + * The layer orientation. The following values are supported:<ul> + * + * <li>bottom-left == bottom + * <li>bottom-right + * <li>top-left == top + * <li>top-right + * <li>left-top + * <li>left-bottom == left + * <li>right-top + * <li>right-bottom == right + * + * </ul>. The default value is "bottom-left", which means that the layers will + * be built from the bottom-up, and the values within layers will be laid out + * from left-to-right. + * + * <p>Note that with non-zero baselines, some orientations may give similar + * results. For example, offset("silohouette") centers the layers, resulting in + * a streamgraph. Thus, the orientations "bottom-left" and "top-left" will + * produce similar results, differing only in the layer order. + * + * @type string + * @name pv.Layout.Stack.prototype.orient + */ + +/** + * The layer order. The following values are supported:<ul> + * + * <li><i>null</i> - use given layer order. + * <li>inside-out - sort by maximum value, with balanced order. + * <li>reverse - use reverse of given layer order. + * + * </ul>For details on the inside-out order algorithm, refer to "Stacked Graphs + * -- Geometry & Aesthetics" by L. Byron and M. Wattenberg, IEEE TVCG + * November/December 2008. + * + * @type string + * @name pv.Layout.Stack.prototype.order + */ + +/** + * The layer offset; the y-position of the bottom of the lowest layer. The + * following values are supported:<ul> + * + * <li>zero - use a zero baseline, i.e., the y-axis. + * <li>silohouette - center the stream, i.e., ThemeRiver. + * <li>wiggle - minimize weighted change in slope. + * <li>expand - expand layers to fill the enclosing layout dimensions. + * + * </ul>For details on these offset algorithms, refer to "Stacked Graphs -- + * Geometry & Aesthetics" by L. Byron and M. Wattenberg, IEEE TVCG + * November/December 2008. + * + * @type string + * @name pv.Layout.Stack.prototype.offset + */ +/** + * Constructs a new, empty treemap layout. Layouts are not typically + * constructed directly; instead, they are added to an existing panel via + * {@link pv.Mark#add}. + * + * @class Implements a space-filling rectangular layout, with the hierarchy + * represented via containment. Treemaps represent nodes as boxes, with child + * nodes placed within parent boxes. The size of each box is proportional to the + * size of the node in the tree. This particular algorithm is taken from Bruls, + * D.M., C. Huizing, and J.J. van Wijk, <a + * href="http://www.win.tue.nl/~vanwijk/stm.pdf">"Squarified Treemaps"</a> in + * <i>Data Visualization 2000, Proceedings of the Joint Eurographics and IEEE + * TCVG Sumposium on Visualization</i>, 2000, pp. 33-42. + * + * <p>The meaning of the exported mark prototypes changes slightly in the + * space-filling implementation:<ul> + * + * <li><tt>node</tt> - for rendering nodes; typically a {@link pv.Bar}. The node + * data is populated with <tt>dx</tt> and <tt>dy</tt> attributes, in addition to + * the standard <tt>x</tt> and <tt>y</tt> position attributes. + * + * <p><li><tt>leaf</tt> - for rendering leaf nodes only, with no fill or stroke + * style by default; typically a {@link pv.Panel} or another layout! + * + * <p><li><tt>link</tt> - unsupported; undefined. Links are encoded implicitly + * in the arrangement of the space-filling nodes. + * + * <p><li><tt>label</tt> - for rendering node labels; typically a + * {@link pv.Label}. + * + * </ul>For more details on how to use this layout, see + * {@link pv.Layout.Hierarchy}. + * + * @extends pv.Layout.Hierarchy + */ +pv.Layout.Treemap = function() { + pv.Layout.Hierarchy.call(this); + + this.node + .strokeStyle("#fff") + .fillStyle("rgba(31, 119, 180, .25)") + .width(function(n) { return n.dx; }) + .height(function(n) { return n.dy; }); + + this.label + .visible(function(n) { return !n.firstChild; }) + .left(function(n) { return n.x + (n.dx / 2); }) + .top(function(n) { return n.y + (n.dy / 2); }) + .textAlign("center") + .textAngle(function(n) { return n.dx > n.dy ? 0 : -Math.PI / 2; }); + + (this.leaf = new pv.Mark() + .extend(this.node) + .fillStyle(null) + .strokeStyle(null) + .visible(function(n) { return !n.firstChild; })).parent = this; + + /* Hide unsupported link. */ + delete this.link; +}; + +pv.Layout.Treemap.prototype = pv.extend(pv.Layout.Hierarchy) + .property("round", Boolean) + .property("paddingLeft", Number) + .property("paddingRight", Number) + .property("paddingTop", Number) + .property("paddingBottom", Number) + .property("mode", String) + .property("order", String); + +/** + * Default propertiess for treemap layouts. The default mode is "squarify" and + * the default order is "ascending". + * + * @type pv.Layout.Treemap + */ +pv.Layout.Treemap.prototype.defaults = new pv.Layout.Treemap() + .extend(pv.Layout.Hierarchy.prototype.defaults) + .mode("squarify") // squarify, slice-and-dice, slice, dice + .order("ascending"); // ascending, descending, reverse, null + +/** + * Whether node sizes should be rounded to integer values. This has a similar + * effect to setting <tt>antialias(false)</tt> for node values, but allows the + * treemap algorithm to accumulate error related to pixel rounding. + * + * @type boolean + * @name pv.Layout.Treemap.prototype.round + */ + +/** + * The left inset between parent add child in pixels. Defaults to 0. + * + * @type number + * @name pv.Layout.Treemap.prototype.paddingLeft + * @see #padding + */ + +/** + * The right inset between parent add child in pixels. Defaults to 0. + * + * @type number + * @name pv.Layout.Treemap.prototype.paddingRight + * @see #padding + */ + +/** + * The top inset between parent and child in pixels. Defaults to 0. + * + * @type number + * @name pv.Layout.Treemap.prototype.paddingTop + * @see #padding + */ + +/** + * The bottom inset between parent and child in pixels. Defaults to 0. + * + * @type number + * @name pv.Layout.Treemap.prototype.paddingBottom + * @see #padding + */ + +/** + * The treemap algorithm. The default value is "squarify". The "slice-and-dice" + * algorithm may also be used, which alternates between horizontal and vertical + * slices for different depths. In addition, the "slice" and "dice" algorithms + * may be specified explicitly to control whether horizontal or vertical slices + * are used, which may be useful for nested treemap layouts. + * + * @type string + * @name pv.Layout.Treemap.prototype.mode + * @see <a + * href="ftp://ftp.cs.umd.edu/pub/hcil/Reports-Abstracts-Bibliography/2001-06html/2001-06.pdf" + * >"Ordered Treemap Layouts"</a> by B. Shneiderman & M. Wattenberg, IEEE + * InfoVis 2001. + */ + +/** + * The sibling node order. A <tt>null</tt> value means to use the sibling order + * specified by the nodes property as-is; "reverse" will reverse the given + * order. The default value "ascending" will sort siblings in ascending order of + * size, while "descending" will do the reverse. For sorting based on data + * attributes other than size, use the default <tt>null</tt> for the order + * property, and sort the nodes beforehand using the {@link pv.Dom} operator. + * + * @type string + * @name pv.Layout.Treemap.prototype.order + */ + +/** + * Alias for setting the left, right, top and bottom padding properties + * simultaneously. + * + * @see #paddingLeft + * @see #paddingRight + * @see #paddingTop + * @see #paddingBottom + * @returns {pv.Layout.Treemap} this. + */ +pv.Layout.Treemap.prototype.padding = function(n) { + return this.paddingLeft(n).paddingRight(n).paddingTop(n).paddingBottom(n); +}; + +/** @private The default size function. */ +pv.Layout.Treemap.prototype.$size = function(d) { + return Number(d.nodeValue); +}; + +/** + * Specifies the sizing function. By default, the size function uses the + * <tt>nodeValue</tt> attribute of nodes as a numeric value: <tt>function(d) + * Number(d.nodeValue)</tt>. + * + * <p>The sizing function is invoked for each leaf node in the tree, per the + * <tt>nodes</tt> property. For example, if the tree data structure represents a + * file system, with files as leaf nodes, and each file has a <tt>bytes</tt> + * attribute, you can specify a size function as: + * + * <pre> .size(function(d) d.bytes)</pre> + * + * @param {function} f the new sizing function. + * @returns {pv.Layout.Treemap} this. + */ +pv.Layout.Treemap.prototype.size = function(f) { + this.$size = pv.functor(f); + return this; +}; + +/** @private */ +pv.Layout.Treemap.prototype.buildImplied = function(s) { + if (pv.Layout.Hierarchy.prototype.buildImplied.call(this, s)) return; + + var that = this, + nodes = s.nodes, + root = nodes[0], + stack = pv.Mark.stack, + left = s.paddingLeft, + right = s.paddingRight, + top = s.paddingTop, + bottom = s.paddingBottom, + /** @ignore */ size = function(n) { return n.size; }, + round = s.round ? Math.round : Number, + mode = s.mode; + + /** @private */ + function slice(row, sum, horizontal, x, y, w, h) { + for (var i = 0, d = 0; i < row.length; i++) { + var n = row[i]; + if (horizontal) { + n.x = x + d; + n.y = y; + d += n.dx = round(w * n.size / sum); + n.dy = h; + } else { + n.x = x; + n.y = y + d; + n.dx = w; + d += n.dy = round(h * n.size / sum); + } + } + if (n) { // correct on-axis rounding error + if (horizontal) { + n.dx += w - d; + } else { + n.dy += h - d; + } + } + } + + /** @private */ + function ratio(row, l) { + var rmax = -Infinity, rmin = Infinity, s = 0; + for (var i = 0; i < row.length; i++) { + var r = row[i].size; + if (r < rmin) rmin = r; + if (r > rmax) rmax = r; + s += r; + } + s = s * s; + l = l * l; + return Math.max(l * rmax / s, s / (l * rmin)); + } + + /** @private */ + function layout(n, i) { + var x = n.x + left, + y = n.y + top, + w = n.dx - left - right, + h = n.dy - top - bottom; + + /* Assume squarify by default. */ + if (mode != "squarify") { + slice(n.childNodes, n.size, + mode == "slice" ? true + : mode == "dice" ? false + : i & 1, x, y, w, h); + return; + } + + var row = [], + mink = Infinity, + l = Math.min(w, h), + k = w * h / n.size; + + /* Abort if the size is nonpositive. */ + if (n.size <= 0) return; + + /* Scale the sizes to fill the current subregion. */ + n.visitBefore(function(n) { n.size *= k; }); + + /** @private Position the specified nodes along one dimension. */ + function position(row) { + var horizontal = w == l, + sum = pv.sum(row, size), + r = l ? round(sum / l) : 0; + slice(row, sum, horizontal, x, y, horizontal ? w : r, horizontal ? r : h); + if (horizontal) { + y += r; + h -= r; + } else { + x += r; + w -= r; + } + l = Math.min(w, h); + return horizontal; + } + + var children = n.childNodes.slice(); // copy + while (children.length) { + var child = children[children.length - 1]; + if (!child.size) { + children.pop(); + continue; + } + row.push(child); + + var k = ratio(row, l); + if (k <= mink) { + children.pop(); + mink = k; + } else { + row.pop(); + position(row); + row.length = 0; + mink = Infinity; + } + } + + /* correct off-axis rounding error */ + if (position(row)) for (var i = 0; i < row.length; i++) { + row[i].dy += h; + } else for (var i = 0; i < row.length; i++) { + row[i].dx += w; + } + } + + /* Recursively compute the node depth and size. */ + stack.unshift(null); + root.visitAfter(function(n, i) { + n.depth = i; + n.x = n.y = n.dx = n.dy = 0; + n.size = n.firstChild + ? pv.sum(n.childNodes, function(n) { return n.size; }) + : that.$size.apply(that, (stack[0] = n, stack)); + }); + stack.shift(); + + /* Sort. */ + switch (s.order) { + case "ascending": { + root.sort(function(a, b) { return a.size - b.size; }); + break; + } + case "descending": { + root.sort(function(a, b) { return b.size - a.size; }); + break; + } + case "reverse": root.reverse(); break; + } + + /* Recursively compute the layout. */ + root.x = 0; + root.y = 0; + root.dx = s.width; + root.dy = s.height; + root.visitBefore(layout); +}; +/** + * Constructs a new, empty tree layout. Layouts are not typically constructed + * directly; instead, they are added to an existing panel via + * {@link pv.Mark#add}. + * + * @class Implements a node-link tree diagram using the Reingold-Tilford "tidy" + * tree layout algorithm. The specific algorithm used by this layout is based on + * <a href="http://citeseer.ist.psu.edu/buchheim02improving.html">"Improving + * Walker's Algorithm to Run in Linear Time"</A> by C. Buchheim, M. Jünger + * & S. Leipert, Graph Drawing 2002. This layout supports both cartesian and + * radial orientations orientations for node-link diagrams. + * + * <p>The tree layout supports a "group" property, which if true causes siblings + * to be positioned closer together than unrelated nodes at the same depth. The + * layout can be configured using the <tt>depth</tt> and <tt>breadth</tt> + * properties, which control the increments in pixel space between nodes in both + * dimensions, similar to the indent layout. + * + * <p>For more details on how to use this layout, see + * {@link pv.Layout.Hierarchy}. + * + * @extends pv.Layout.Hierarchy + */ +pv.Layout.Tree = function() { + pv.Layout.Hierarchy.call(this); +}; + +pv.Layout.Tree.prototype = pv.extend(pv.Layout.Hierarchy) + .property("group", Number) + .property("breadth", Number) + .property("depth", Number) + .property("orient", String); + +/** + * Default properties for tree layouts. The default orientation is "top", the + * default group parameter is 1, and the default breadth and depth offsets are + * 15 and 60 respectively. + * + * @type pv.Layout.Tree + */ +pv.Layout.Tree.prototype.defaults = new pv.Layout.Tree() + .extend(pv.Layout.Hierarchy.prototype.defaults) + .group(1) + .breadth(15) + .depth(60) + .orient("top"); + +/** @private */ +pv.Layout.Tree.prototype.buildImplied = function(s) { + if (pv.Layout.Hierarchy.prototype.buildImplied.call(this, s)) return; + + var nodes = s.nodes, + orient = s.orient, + depth = s.depth, + breadth = s.breadth, + group = s.group, + w = s.width, + h = s.height; + + /** @private */ + function firstWalk(v) { + var l, r, a; + if (!v.firstChild) { + if (l = v.previousSibling) { + v.prelim = l.prelim + distance(v.depth, true); + } + } else { + l = v.firstChild; + r = v.lastChild; + a = l; // default ancestor + for (var c = l; c; c = c.nextSibling) { + firstWalk(c); + a = apportion(c, a); + } + executeShifts(v); + var midpoint = .5 * (l.prelim + r.prelim); + if (l = v.previousSibling) { + v.prelim = l.prelim + distance(v.depth, true); + v.mod = v.prelim - midpoint; + } else { + v.prelim = midpoint; + } + } + } + + /** @private */ + function secondWalk(v, m, depth) { + v.breadth = v.prelim + m; + m += v.mod; + for (var c = v.firstChild; c; c = c.nextSibling) { + secondWalk(c, m, depth); + } + } + + /** @private */ + function apportion(v, a) { + var w = v.previousSibling; + if (w) { + var vip = v, + vop = v, + vim = w, + vom = v.parentNode.firstChild, + sip = vip.mod, + sop = vop.mod, + sim = vim.mod, + som = vom.mod, + nr = nextRight(vim), + nl = nextLeft(vip); + while (nr && nl) { + vim = nr; + vip = nl; + vom = nextLeft(vom); + vop = nextRight(vop); + vop.ancestor = v; + var shift = (vim.prelim + sim) - (vip.prelim + sip) + distance(vim.depth, false); + if (shift > 0) { + moveSubtree(ancestor(vim, v, a), v, shift); + sip += shift; + sop += shift; + } + sim += vim.mod; + sip += vip.mod; + som += vom.mod; + sop += vop.mod; + nr = nextRight(vim); + nl = nextLeft(vip); + } + if (nr && !nextRight(vop)) { + vop.thread = nr; + vop.mod += sim - sop; + } + if (nl && !nextLeft(vom)) { + vom.thread = nl; + vom.mod += sip - som; + a = v; + } + } + return a; + } + + /** @private */ + function nextLeft(v) { + return v.firstChild || v.thread; + } + + /** @private */ + function nextRight(v) { + return v.lastChild || v.thread; + } + + /** @private */ + function moveSubtree(wm, wp, shift) { + var subtrees = wp.number - wm.number; + wp.change -= shift / subtrees; + wp.shift += shift; + wm.change += shift / subtrees; + wp.prelim += shift; + wp.mod += shift; + } + + /** @private */ + function executeShifts(v) { + var shift = 0, change = 0; + for (var c = v.lastChild; c; c = c.previousSibling) { + c.prelim += shift; + c.mod += shift; + change += c.change; + shift += c.shift + change; + } + } + + /** @private */ + function ancestor(vim, v, a) { + return (vim.ancestor.parentNode == v.parentNode) ? vim.ancestor : a; + } + + /** @private */ + function distance(depth, siblings) { + return (siblings ? 1 : (group + 1)) / ((orient == "radial") ? depth : 1); + } + + /* Initialize temporary layout variables. TODO: store separately. */ + var root = nodes[0]; + root.visitAfter(function(v, i) { + v.ancestor = v; + v.prelim = 0; + v.mod = 0; + v.change = 0; + v.shift = 0; + v.number = v.previousSibling ? (v.previousSibling.number + 1) : 0; + v.depth = i; + }); + + /* Compute the layout using Buchheim et al.'s algorithm. */ + firstWalk(root); + secondWalk(root, -root.prelim, 0); + + /** @private Returns the angle of the given node. */ + function midAngle(n) { + return (orient == "radial") ? n.breadth / depth : 0; + } + + /** @private */ + function x(n) { + switch (orient) { + case "left": return n.depth; + case "right": return w - n.depth; + case "top": + case "bottom": return n.breadth + w / 2; + case "radial": return w / 2 + n.depth * Math.cos(midAngle(n)); + } + } + + /** @private */ + function y(n) { + switch (orient) { + case "left": + case "right": return n.breadth + h / 2; + case "top": return n.depth; + case "bottom": return h - n.depth; + case "radial": return h / 2 + n.depth * Math.sin(midAngle(n)); + } + } + + /* Clear temporary layout variables; transform depth and breadth. */ + root.visitAfter(function(v) { + v.breadth *= breadth; + v.depth *= depth; + v.midAngle = midAngle(v); + v.x = x(v); + v.y = y(v); + if (v.firstChild) v.midAngle += Math.PI; + delete v.breadth; + delete v.depth; + delete v.ancestor; + delete v.prelim; + delete v.mod; + delete v.change; + delete v.shift; + delete v.number; + delete v.thread; + }); +}; + +/** + * The offset between siblings nodes; defaults to 15. + * + * @type number + * @name pv.Layout.Tree.prototype.breadth + */ + +/** + * The offset between parent and child nodes; defaults to 60. + * + * @type number + * @name pv.Layout.Tree.prototype.depth + */ + +/** + * The orientation. The default orientation is "top", which means that the root + * node is placed on the top edge, leaf nodes appear at the bottom, and internal + * nodes are in-between. The following orientations are supported:<ul> + * + * <li>left - left-to-right. + * <li>right - right-to-left. + * <li>top - top-to-bottom. + * <li>bottom - bottom-to-top. + * <li>radial - radially, with the root at the center.</ul> + * + * @type string + * @name pv.Layout.Tree.prototype.orient + */ + +/** + * The sibling grouping, i.e., whether differentiating space is placed between + * sibling groups. The default is 1 (or true), causing sibling leaves to be + * separated by one breadth offset. Setting this to false (or 0) causes + * non-siblings to be adjacent. + * + * @type number + * @name pv.Layout.Tree.prototype.group + */ +/** + * Constructs a new, empty indent layout. Layouts are not typically constructed + * directly; instead, they are added to an existing panel via + * {@link pv.Mark#add}. + * + * @class Implements a hierarchical layout using the indent algorithm. This + * layout implements a node-link diagram where the nodes are presented in + * preorder traversal, and nodes are indented based on their depth from the + * root. This technique is used ubiquitously by operating systems to represent + * file directories; although it requires much vertical space, indented trees + * allow efficient <i>interactive</i> exploration of trees to find a specific + * node. In addition they allow rapid scanning of node labels, and multivariate + * data such as file sizes can be displayed adjacent to the hierarchy. + * + * <p>The indent layout can be configured using the <tt>depth</tt> and + * <tt>breadth</tt> properties, which control the increments in pixel space for + * each indent and row in the layout. This layout does not support multiple + * orientations; the root node is rendered in the top-left, while + * <tt>breadth</tt> is a vertical offset from the top, and <tt>depth</tt> is a + * horizontal offset from the left. + * + * <p>For more details on how to use this layout, see + * {@link pv.Layout.Hierarchy}. + * + * @extends pv.Layout.Hierarchy + */ +pv.Layout.Indent = function() { + pv.Layout.Hierarchy.call(this); + this.link.interpolate("step-after"); +}; + +pv.Layout.Indent.prototype = pv.extend(pv.Layout.Hierarchy) + .property("depth", Number) + .property("breadth", Number); + +/** + * The horizontal offset between different levels of the tree; defaults to 15. + * + * @type number + * @name pv.Layout.Indent.prototype.depth + */ + +/** + * The vertical offset between nodes; defaults to 15. + * + * @type number + * @name pv.Layout.Indent.prototype.breadth + */ + +/** + * Default properties for indent layouts. By default the depth and breadth + * offsets are 15 pixels. + * + * @type pv.Layout.Indent + */ +pv.Layout.Indent.prototype.defaults = new pv.Layout.Indent() + .extend(pv.Layout.Hierarchy.prototype.defaults) + .depth(15) + .breadth(15); + +/** @private */ +pv.Layout.Indent.prototype.buildImplied = function(s) { + if (pv.Layout.Hierarchy.prototype.buildImplied.call(this, s)) return; + + var nodes = s.nodes, + bspace = s.breadth, + dspace = s.depth, + ax = 0, + ay = 0; + + /** @private */ + function position(n, breadth, depth) { + n.x = ax + depth++ * dspace; + n.y = ay + breadth++ * bspace; + n.midAngle = 0; + for (var c = n.firstChild; c; c = c.nextSibling) { + breadth = position(c, breadth, depth); + } + return breadth; + } + + position(nodes[0], 1, 1); +}; +/** + * Constructs a new, empty circle-packing layout. Layouts are not typically + * constructed directly; instead, they are added to an existing panel via + * {@link pv.Mark#add}. + * + * @class Implements a hierarchical layout using circle-packing. The meaning of + * the exported mark prototypes changes slightly in the space-filling + * implementation:<ul> + * + * <li><tt>node</tt> - for rendering nodes; typically a {@link pv.Dot}. + * + * <p><li><tt>link</tt> - unsupported; undefined. Links are encoded implicitly + * in the arrangement of the space-filling nodes. + * + * <p><li><tt>label</tt> - for rendering node labels; typically a + * {@link pv.Label}. + * + * </ul>The pack layout support dynamic sizing for leaf nodes, if a + * {@link #size} psuedo-property is specified. The default size function returns + * 1, causing all leaf nodes to be sized equally, and all internal nodes to be + * sized by the number of leaf nodes they have as descendants. + * + * <p>The size function can be used in conjunction with the order property, + * which allows the nodes to the sorted by the computed size. Note: for sorting + * based on other data attributes, simply use the default <tt>null</tt> for the + * order property, and sort the nodes beforehand using the {@link pv.Dom} + * operator. + * + * <p>For more details on how to use this layout, see + * {@link pv.Layout.Hierarchy}. + * + * @extends pv.Layout.Hierarchy + * @see <a href="http://portal.acm.org/citation.cfm?id=1124772.1124851" + * >"Visualization of large hierarchical data by circle packing"</a> by W. Wang, + * H. Wang, G. Dai, and H. Wang, ACM CHI 2006. + */ +pv.Layout.Pack = function() { + pv.Layout.Hierarchy.call(this); + + this.node + .radius(function(n) { return n.radius; }) + .strokeStyle("rgb(31, 119, 180)") + .fillStyle("rgba(31, 119, 180, .25)"); + + this.label + .textAlign("center"); + + /* Hide unsupported link. */ + delete this.link; +}; + +pv.Layout.Pack.prototype = pv.extend(pv.Layout.Hierarchy) + .property("spacing", Number) + .property("order", String); // ascending, descending, reverse, null + +/** + * Default properties for circle-packing layouts. The default spacing parameter + * is 1 and the default order is "ascending". + * + * @type pv.Layout.Pack + */ +pv.Layout.Pack.prototype.defaults = new pv.Layout.Pack() + .extend(pv.Layout.Hierarchy.prototype.defaults) + .spacing(1) + .order("ascending"); + +/** + * The spacing parameter; defaults to 1, which provides a little bit of padding + * between sibling nodes and the enclosing circle. Larger values increase the + * spacing, by making the sibling nodes smaller; a value of zero makes the leaf + * nodes as large as possible, with no padding on enclosing circles. + * + * @type number + * @name pv.Layout.Pack.prototype.spacing + */ + +/** + * The sibling node order. The default order is <tt>null</tt>, which means to + * use the sibling order specified by the nodes property as-is. A value of + * "ascending" will sort siblings in ascending order of size, while "descending" + * will do the reverse. For sorting based on data attributes other than size, + * use the default <tt>null</tt> for the order property, and sort the nodes + * beforehand using the {@link pv.Dom} operator. + * + * @see pv.Dom.Node#sort + * @type string + * @name pv.Layout.Pack.prototype.order + */ + +/** @private The default size function. */ +pv.Layout.Pack.prototype.$radius = function() { return 1; }; + +// TODO is it possible for spacing to operate in pixel space? +// Right now it appears to be multiples of the smallest radius. + +/** + * Specifies the sizing function. By default, a sizing function is disabled and + * all nodes are given constant size. The sizing function is invoked for each + * leaf node in the tree (passed to the constructor). + * + * <p>For example, if the tree data structure represents a file system, with + * files as leaf nodes, and each file has a <tt>bytes</tt> attribute, you can + * specify a size function as: + * + * <pre> .size(function(d) d.bytes)</pre> + * + * As with other properties, a size function may specify additional arguments to + * access the data associated with the layout and any enclosing panels. + * + * @param {function} f the new sizing function. + * @returns {pv.Layout.Pack} this. + */ +pv.Layout.Pack.prototype.size = function(f) { + this.$radius = typeof f == "function" + ? function() { return Math.sqrt(f.apply(this, arguments)); } + : (f = Math.sqrt(f), function() { return f; }); + return this; +}; + +/** @private */ +pv.Layout.Pack.prototype.buildImplied = function(s) { + if (pv.Layout.Hierarchy.prototype.buildImplied.call(this, s)) return; + + var that = this, + nodes = s.nodes, + root = nodes[0]; + + /** @private Compute the radii of the leaf nodes. */ + function radii(nodes) { + var stack = pv.Mark.stack; + stack.unshift(null); + for (var i = 0, n = nodes.length; i < n; i++) { + var c = nodes[i]; + if (!c.firstChild) { + c.radius = that.$radius.apply(that, (stack[0] = c, stack)); + } + } + stack.shift(); + } + + /** @private */ + function packTree(n) { + var nodes = []; + for (var c = n.firstChild; c; c = c.nextSibling) { + if (c.firstChild) c.radius = packTree(c); + c.n = c.p = c; + nodes.push(c); + } + + /* Sort. */ + switch (s.order) { + case "ascending": { + nodes.sort(function(a, b) { return a.radius - b.radius; }); + break; + } + case "descending": { + nodes.sort(function(a, b) { return b.radius - a.radius; }); + break; + } + case "reverse": nodes.reverse(); break; + } + + return packCircle(nodes); + } + + /** @private */ + function packCircle(nodes) { + var xMin = Infinity, + xMax = -Infinity, + yMin = Infinity, + yMax = -Infinity, + a, b, c, j, k; + + /** @private */ + function bound(n) { + xMin = Math.min(n.x - n.radius, xMin); + xMax = Math.max(n.x + n.radius, xMax); + yMin = Math.min(n.y - n.radius, yMin); + yMax = Math.max(n.y + n.radius, yMax); + } + + /** @private */ + function insert(a, b) { + var c = a.n; + a.n = b; + b.p = a; + b.n = c; + c.p = b; + } + + /** @private */ + function splice(a, b) { + a.n = b; + b.p = a; + } + + /** @private */ + function intersects(a, b) { + var dx = b.x - a.x, + dy = b.y - a.y, + dr = a.radius + b.radius; + return (dr * dr - dx * dx - dy * dy) > .001; // within epsilon + } + + /* Create first node. */ + a = nodes[0]; + a.x = -a.radius; + a.y = 0; + bound(a); + + /* Create second node. */ + if (nodes.length > 1) { + b = nodes[1]; + b.x = b.radius; + b.y = 0; + bound(b); + + /* Create third node and build chain. */ + if (nodes.length > 2) { + c = nodes[2]; + place(a, b, c); + bound(c); + insert(a, c); + a.p = c; + insert(c, b); + b = a.n; + + /* Now iterate through the rest. */ + for (var i = 3; i < nodes.length; i++) { + place(a, b, c = nodes[i]); + + /* Search for the closest intersection. */ + var isect = 0, s1 = 1, s2 = 1; + for (j = b.n; j != b; j = j.n, s1++) { + if (intersects(j, c)) { + isect = 1; + break; + } + } + if (isect == 1) { + for (k = a.p; k != j.p; k = k.p, s2++) { + if (intersects(k, c)) { + if (s2 < s1) { + isect = -1; + j = k; + } + break; + } + } + } + + /* Update node chain. */ + if (isect == 0) { + insert(a, c); + b = c; + bound(c); + } else if (isect > 0) { + splice(a, j); + b = j; + i--; + } else if (isect < 0) { + splice(j, b); + a = j; + i--; + } + } + } + } + + /* Re-center the circles and return the encompassing radius. */ + var cx = (xMin + xMax) / 2, + cy = (yMin + yMax) / 2, + cr = 0; + for (var i = 0; i < nodes.length; i++) { + var n = nodes[i]; + n.x -= cx; + n.y -= cy; + cr = Math.max(cr, n.radius + Math.sqrt(n.x * n.x + n.y * n.y)); + } + return cr + s.spacing; + } + + /** @private */ + function place(a, b, c) { + var da = b.radius + c.radius, + db = a.radius + c.radius, + dx = b.x - a.x, + dy = b.y - a.y, + dc = Math.sqrt(dx * dx + dy * dy), + cos = (db * db + dc * dc - da * da) / (2 * db * dc), + theta = Math.acos(cos), + x = cos * db, + h = Math.sin(theta) * db; + dx /= dc; + dy /= dc; + c.x = a.x + x * dx + h * dy; + c.y = a.y + x * dy - h * dx; + } + + /** @private */ + function transform(n, x, y, k) { + for (var c = n.firstChild; c; c = c.nextSibling) { + c.x += n.x; + c.y += n.y; + transform(c, x, y, k); + } + n.x = x + k * n.x; + n.y = y + k * n.y; + n.radius *= k; + } + + radii(nodes); + + /* Recursively compute the layout. */ + root.x = 0; + root.y = 0; + root.radius = packTree(root); + + var w = this.width(), + h = this.height(), + k = 1 / Math.max(2 * root.radius / w, 2 * root.radius / h); + transform(root, w / 2, h / 2, k); +}; +/** + * Constructs a new, empty force-directed layout. Layouts are not typically + * constructed directly; instead, they are added to an existing panel via + * {@link pv.Mark#add}. + * + * @class Implements force-directed network layout as a node-link diagram. This + * layout uses the Fruchterman-Reingold algorithm, which applies an attractive + * spring force between neighboring nodes, and a repulsive electrical charge + * force between all nodes. An additional drag force improves stability of the + * simulation. See {@link pv.Force.spring}, {@link pv.Force.drag} and {@link + * pv.Force.charge} for more details; note that the n-body charge force is + * approximated using the Barnes-Hut algorithm. + * + * <p>This layout is implemented on top of {@link pv.Simulation}, which can be + * used directly for more control over simulation parameters. The simulation + * uses Position Verlet integration, which does not compute velocities + * explicitly, but allows for easy geometric constraints, such as bounding the + * nodes within the layout panel. Many of the configuration properties supported + * by this layout are simply passed through to the underlying forces and + * constraints of the simulation. + * + * <p>Force layouts are typically interactive. The gradual movement of the nodes + * as they stabilize to a local stress minimum can help reveal the structure of + * the network, as can {@link pv.Behavior.drag}, which allows the user to pick + * up nodes and reposition them while the physics simulation continues. This + * layout can also be used with pan & zoom behaviors for interaction. + * + * <p>To facilitate interaction, this layout by default automatically re-renders + * using a <tt>setInterval</tt> every 42 milliseconds. This can be disabled via + * the <tt>iterations</tt> property, which if non-null specifies the number of + * simulation iterations to run before the force-directed layout is finalized. + * Be careful not to use too high an iteration count, as this can lead to an + * annoying delay on page load. + * + * <p>As with other network layouts, the network data can be updated + * dynamically, provided the property cache is reset. See + * {@link pv.Layout.Network} for details. New nodes are initialized with random + * positions near the center. Alternatively, positions can be specified manually + * by setting the <tt>x</tt> and <tt>y</tt> attributes on nodes. + * + * @extends pv.Layout.Network + * @see <a href="http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.13.8444&rep=rep1&type=pdf" + * >"Graph Drawing by Force-directed Placement"</a> by T. Fruchterman & + * E. Reingold, Software--Practice & Experience, November 1991. + */ +pv.Layout.Force = function() { + pv.Layout.Network.call(this); + + /* Force-directed graphs can be messy, so reduce the link width. */ + this.link.lineWidth(function(d, p) { return Math.sqrt(p.linkValue) * 1.5; }); + this.label.textAlign("center"); +}; + +pv.Layout.Force.prototype = pv.extend(pv.Layout.Network) + .property("bound", Boolean) + .property("iterations", Number) + .property("dragConstant", Number) + .property("chargeConstant", Number) + .property("chargeMinDistance", Number) + .property("chargeMaxDistance", Number) + .property("chargeTheta", Number) + .property("springConstant", Number) + .property("springDamping", Number) + .property("springLength", Number); + +/** + * The bound parameter; true if nodes should be constrained within the layout + * panel. Bounding is disabled by default. Currently the layout does not observe + * the radius of the nodes; strictly speaking, only the center of the node is + * constrained to be within the panel, with an additional 6-pixel offset for + * padding. A future enhancement could extend the bound constraint to observe + * the node's radius, which would also support bounding for variable-size nodes. + * + * <p>Note that if this layout is used in conjunction with pan & zoom + * behaviors, those behaviors should have their bound parameter set to the same + * value. + * + * @type boolean + * @name pv.Layout.Force.prototype.bound + */ + +/** + * The number of simulation iterations to run, or null if this layout is + * interactive. Force-directed layouts are interactive by default, using a + * <tt>setInterval</tt> to advance the physics simulation and re-render + * automatically. + * + * @type number + * @name pv.Layout.Force.prototype.iterations + */ + +/** + * The drag constant, in the range [0,1]. A value of 0 means no drag (a + * perfectly frictionless environment), while a value of 1 means friction + * immediately cancels all momentum. The default value is 0.1, which provides a + * minimum amount of drag that helps stabilize bouncy springs; lower values may + * result in excessive bounciness, while higher values cause the simulation to + * take longer to converge. + * + * @type number + * @name pv.Layout.Force.prototype.dragConstant + * @see pv.Force.drag#constant + */ + +/** + * The charge constant, which should be a negative number. The default value is + * -40; more negative values will result in a stronger repulsive force, which + * may lead to faster convergence at the risk of instability. Too strong + * repulsive charge forces can cause comparatively weak springs to be stretched + * well beyond their rest length, emphasizing global structure over local + * structure. A nonnegative value will break the Fruchterman-Reingold algorithm, + * and is for entertainment purposes only. + * + * @type number + * @name pv.Layout.Force.prototype.chargeConstant + * @see pv.Force.charge#constant + */ + +/** + * The minimum distance at which charge forces are applied. The default minimum + * distance of 2 avoids applying forces that are two strong; because the physics + * simulation is run at discrete time intervals, it is possible for two same- + * charged particles to become very close or even a singularity! Since the + * charge force is inversely proportional to the square of the distance, very + * small distances can break the simulation. + * + * <p>In rare cases, two particles can become stuck on top of each other, as a + * minimum distance threshold will prevent the charge force from repelling them. + * However, this occurs very rarely because other forces and momentum typically + * cause the particles to become separated again, at which point the repulsive + * charge force kicks in. + * + * @type number + * @name pv.Layout.Force.prototype.chargeMinDistance + * @see pv.Force.charge#domain + */ + +/** + * The maximum distance at which charge forces are applied. This improves + * performance by ignoring weak charge forces at great distances. Note that this + * parameter is partly redundant, as the Barnes-Hut algorithm for n-body forces + * already improves performance for far-away particles through approximation. + * + * @type number + * @name pv.Layout.Force.prototype.chargeMaxDistance + * @see pv.Force.charge#domain + */ + +/** + * The Barnes-Hut approximation factor. The Barnes-Hut approximation criterion + * is the ratio of the size of the quadtree node to the distance from the point + * to the node's center of mass is beneath some threshold. The default value is + * 0.9. + * + * @type number + * @name pv.Layout.Force.prototype.chargeTheta + * @see pv.Force.charge#theta + */ + +/** + * The spring constant, which should be a positive number. The default value is + * 0.1; greater values will result in a stronger attractive force, which may + * lead to faster convergence at the risk of instability. Too strong spring + * forces can cause comparatively weak charge forces to be ignored, emphasizing + * local structure over global structure. A nonpositive value will break the + * Fruchterman-Reingold algorithm, and is for entertainment purposes only. + * + * <p>The spring tension is automatically normalized using the inverse square + * root of the maximum link degree of attached nodes. + * + * @type number + * @name pv.Layout.Force.prototype.springConstant + * @see pv.Force.spring#constant + */ + +/** + * The spring damping factor, in the range [0,1]. Damping functions identically + * to drag forces, damping spring bounciness by applying a force in the opposite + * direction of attached nodes' velocities. The default value is 0.3. + * + * <p>The spring damping is automatically normalized using the inverse square + * root of the maximum link degree of attached nodes. + * + * @type number + * @name pv.Layout.Force.prototype.springDamping + * @see pv.Force.spring#damping + */ + +/** + * The spring rest length. The default value is 20 pixels. Larger values may be + * appropriate if the layout panel is larger, or if the nodes are rendered + * larger than the default dot size of 20. + * + * @type number + * @name pv.Layout.Force.prototype.springLength + * @see pv.Force.spring#length + */ + +/** + * Default properties for force-directed layouts. The default drag constant is + * 0.1, the default charge constant is -40 (with a domain of [2, 500] and theta + * of 0.9), and the default spring constant is 0.1 (with a damping of 0.3 and a + * rest length of 20). + * + * @type pv.Layout.Force + */ +pv.Layout.Force.prototype.defaults = new pv.Layout.Force() + .extend(pv.Layout.Network.prototype.defaults) + .dragConstant(.1) + .chargeConstant(-40) + .chargeMinDistance(2) + .chargeMaxDistance(500) + .chargeTheta(.9) + .springConstant(.1) + .springDamping(.3) + .springLength(20); + +/** @private Initialize the physics simulation. */ +pv.Layout.Force.prototype.buildImplied = function(s) { + + /* Any cached interactive layouts need to be rebound for the timer. */ + if (pv.Layout.Network.prototype.buildImplied.call(this, s)) { + var f = s.$force; + if (f) { + f.next = this.binds.$force; + this.binds.$force = f; + } + return; + } + + var that = this, + nodes = s.nodes, + links = s.links, + k = s.iterations, + w = s.width, + h = s.height; + + /* Initialize positions randomly near the center. */ + for (var i = 0, n; i < nodes.length; i++) { + n = nodes[i]; + if (isNaN(n.x)) n.x = w / 2 + 40 * Math.random() - 20; + if (isNaN(n.y)) n.y = h / 2 + 40 * Math.random() - 20; + } + + /* Initialize the simulation. */ + var sim = pv.simulation(nodes); + + /* Drag force. */ + sim.force(pv.Force.drag(s.dragConstant)); + + /* Charge (repelling) force. */ + sim.force(pv.Force.charge(s.chargeConstant) + .domain(s.chargeMinDistance, s.chargeMaxDistance) + .theta(s.chargeTheta)); + + /* Spring (attracting) force. */ + sim.force(pv.Force.spring(s.springConstant) + .damping(s.springDamping) + .length(s.springLength) + .links(links)); + + /* Position constraint (for interactive dragging). */ + sim.constraint(pv.Constraint.position()); + + /* Optionally add bound constraint. TODO: better padding. */ + if (s.bound) { + sim.constraint(pv.Constraint.bound().x(6, w - 6).y(6, h - 6)); + } + + /** @private Returns the speed of the given node, to determine cooling. */ + function speed(n) { + return n.fix ? 1 : n.vx * n.vx + n.vy * n.vy; + } + + /* + * If the iterations property is null (the default), the layout is + * interactive. The simulation is run until the fastest particle drops below + * an arbitrary minimum speed. Although the timer keeps firing, this speed + * calculation is fast so there is minimal CPU overhead. Note: if a particle + * is fixed for interactivity, treat this as a high speed and resume + * simulation. + */ + if (k == null) { + sim.step(); // compute initial previous velocities + sim.step(); // compute initial velocities + + /* Add the simulation state to the bound list. */ + var force = s.$force = this.binds.$force = { + next: this.binds.$force, + nodes: nodes, + min: 1e-4 * (links.length + 1), + sim: sim + }; + + /* Start the timer, if not already started. */ + if (!this.$timer) this.$timer = setInterval(function() { + var render = false; + for (var f = that.binds.$force; f; f = f.next) { + if (pv.max(f.nodes, speed) > f.min) { + f.sim.step(); + render = true; + } + } + if (render) that.render(); + }, 42); + } else for (var i = 0; i < k; i++) { + sim.step(); + } +}; +/** + * Constructs a new, empty cluster layout. Layouts are not typically + * constructed directly; instead, they are added to an existing panel via + * {@link pv.Mark#add}. + * + * @class Implements a hierarchical layout using the cluster (or dendrogram) + * algorithm. This layout provides both node-link and space-filling + * implementations of cluster diagrams. In many ways it is similar to + * {@link pv.Layout.Partition}, except that leaf nodes are positioned at maximum + * depth, and the depth of internal nodes is based on their distance from their + * deepest descendant, rather than their distance from the root. + * + * <p>The cluster layout supports a "group" property, which if true causes + * siblings to be positioned closer together than unrelated nodes at the same + * depth. Unlike the partition layout, this layout does not support dynamic + * sizing for leaf nodes; all leaf nodes are the same size. + * + * <p>For more details on how to use this layout, see + * {@link pv.Layout.Hierarchy}. + * + * @see pv.Layout.Cluster.Fill + * @extends pv.Layout.Hierarchy + */ +pv.Layout.Cluster = function() { + pv.Layout.Hierarchy.call(this); + var interpolate, // cached interpolate + buildImplied = this.buildImplied; + + /** @private Cache layout state to optimize properties. */ + this.buildImplied = function(s) { + buildImplied.call(this, s); + interpolate + = /^(top|bottom)$/.test(s.orient) ? "step-before" + : /^(left|right)$/.test(s.orient) ? "step-after" + : "linear"; + }; + + this.link.interpolate(function() { return interpolate; }); +}; + +pv.Layout.Cluster.prototype = pv.extend(pv.Layout.Hierarchy) + .property("group", Number) + .property("orient", String) + .property("innerRadius", Number) + .property("outerRadius", Number); + +/** + * The group parameter; defaults to 0, disabling grouping of siblings. If this + * parameter is set to a positive number (or true, which is equivalent to 1), + * then additional space will be allotted between sibling groups. In other + * words, siblings (nodes that share the same parent) will be positioned more + * closely than nodes at the same depth that do not share a parent. + * + * @type number + * @name pv.Layout.Cluster.prototype.group + */ + +/** + * The orientation. The default orientation is "top", which means that the root + * node is placed on the top edge, leaf nodes appear on the bottom edge, and + * internal nodes are in-between. The following orientations are supported:<ul> + * + * <li>left - left-to-right. + * <li>right - right-to-left. + * <li>top - top-to-bottom. + * <li>bottom - bottom-to-top. + * <li>radial - radially, with the root at the center.</ul> + * + * @type string + * @name pv.Layout.Cluster.prototype.orient + */ + +/** + * The inner radius; defaults to 0. This property applies only to radial + * orientations, and can be used to compress the layout radially. Note that for + * the node-link implementation, the root node is always at the center, + * regardless of the value of this property; this property only affects internal + * and leaf nodes. For the space-filling implementation, a non-zero value of + * this property will result in the root node represented as a ring rather than + * a circle. + * + * @type number + * @name pv.Layout.Cluster.prototype.innerRadius + */ + +/** + * The outer radius; defaults to fill the containing panel, based on the height + * and width of the layout. If the layout has no height and width specified, it + * will extend to fill the enclosing panel. + * + * @type number + * @name pv.Layout.Cluster.prototype.outerRadius + */ + +/** + * Defaults for cluster layouts. The default group parameter is 0 and the + * default orientation is "top". + * + * @type pv.Layout.Cluster + */ +pv.Layout.Cluster.prototype.defaults = new pv.Layout.Cluster() + .extend(pv.Layout.Hierarchy.prototype.defaults) + .group(0) + .orient("top"); + +/** @private */ +pv.Layout.Cluster.prototype.buildImplied = function(s) { + if (pv.Layout.Hierarchy.prototype.buildImplied.call(this, s)) return; + + var root = s.nodes[0], + group = s.group, + breadth, + depth, + leafCount = 0, + leafIndex = .5 - group / 2; + + /* Count the leaf nodes and compute the depth of descendants. */ + var p = undefined; + root.visitAfter(function(n) { + if (n.firstChild) { + n.depth = 1 + pv.max(n.childNodes, function(n) { return n.depth; }); + } else { + if (group && (p != n.parentNode)) { + p = n.parentNode; + leafCount += group; + } + leafCount++; + n.depth = 0; + } + }); + breadth = 1 / leafCount; + depth = 1 / root.depth; + + /* Compute the unit breadth and depth of each node. */ + var p = undefined; + root.visitAfter(function(n) { + if (n.firstChild) { + n.breadth = pv.mean(n.childNodes, function(n) { return n.breadth; }); + } else { + if (group && (p != n.parentNode)) { + p = n.parentNode; + leafIndex += group; + } + n.breadth = breadth * leafIndex++; + } + n.depth = 1 - n.depth * depth; + }); + + /* Compute breadth and depth ranges for space-filling layouts. */ + root.visitAfter(function(n) { + n.minBreadth = n.firstChild + ? n.firstChild.minBreadth + : (n.breadth - breadth / 2); + n.maxBreadth = n.firstChild + ? n.lastChild.maxBreadth + : (n.breadth + breadth / 2); + }); + root.visitBefore(function(n) { + n.minDepth = n.parentNode + ? n.parentNode.maxDepth + : 0; + n.maxDepth = n.parentNode + ? (n.depth + root.depth) + : (n.minDepth + 2 * root.depth); + }); + root.minDepth = -depth; + + pv.Layout.Hierarchy.NodeLink.buildImplied.call(this, s); +}; + +/** + * Constructs a new, empty space-filling cluster layout. Layouts are not + * typically constructed directly; instead, they are added to an existing panel + * via {@link pv.Mark#add}. + * + * @class A variant of cluster layout that is space-filling. The meaning of the + * exported mark prototypes changes slightly in the space-filling + * implementation:<ul> + * + * <li><tt>node</tt> - for rendering nodes; typically a {@link pv.Bar} for + * non-radial orientations, and a {@link pv.Wedge} for radial orientations. + * + * <p><li><tt>link</tt> - unsupported; undefined. Links are encoded implicitly + * in the arrangement of the space-filling nodes. + * + * <p><li><tt>label</tt> - for rendering node labels; typically a + * {@link pv.Label}. + * + * </ul>For more details on how to use this layout, see + * {@link pv.Layout.Cluster}. + * + * @extends pv.Layout.Cluster + */ +pv.Layout.Cluster.Fill = function() { + pv.Layout.Cluster.call(this); + pv.Layout.Hierarchy.Fill.constructor.call(this); +}; + +pv.Layout.Cluster.Fill.prototype = pv.extend(pv.Layout.Cluster); + +/** @private */ +pv.Layout.Cluster.Fill.prototype.buildImplied = function(s) { + if (pv.Layout.Cluster.prototype.buildImplied.call(this, s)) return; + pv.Layout.Hierarchy.Fill.buildImplied.call(this, s); +}; +/** + * Constructs a new, empty partition layout. Layouts are not typically + * constructed directly; instead, they are added to an existing panel via + * {@link pv.Mark#add}. + * + * @class Implemeents a hierarchical layout using the partition (or sunburst, + * icicle) algorithm. This layout provides both node-link and space-filling + * implementations of partition diagrams. In many ways it is similar to + * {@link pv.Layout.Cluster}, except that leaf nodes are positioned based on + * their distance from the root. + * + * <p>The partition layout support dynamic sizing for leaf nodes, if a + * {@link #size} psuedo-property is specified. The default size function returns + * 1, causing all leaf nodes to be sized equally, and all internal nodes to be + * sized by the number of leaf nodes they have as descendants. + * + * <p>The size function can be used in conjunction with the order property, + * which allows the nodes to the sorted by the computed size. Note: for sorting + * based on other data attributes, simply use the default <tt>null</tt> for the + * order property, and sort the nodes beforehand using the {@link pv.Dom} + * operator. + * + * <p>For more details on how to use this layout, see + * {@link pv.Layout.Hierarchy}. + * + * @see pv.Layout.Partition.Fill + * @extends pv.Layout.Hierarchy + */ +pv.Layout.Partition = function() { + pv.Layout.Hierarchy.call(this); +}; + +pv.Layout.Partition.prototype = pv.extend(pv.Layout.Hierarchy) + .property("order", String) // null, ascending, descending? + .property("orient", String) // top, left, right, bottom, radial + .property("innerRadius", Number) + .property("outerRadius", Number); + +/** + * The sibling node order. The default order is <tt>null</tt>, which means to + * use the sibling order specified by the nodes property as-is. A value of + * "ascending" will sort siblings in ascending order of size, while "descending" + * will do the reverse. For sorting based on data attributes other than size, + * use the default <tt>null</tt> for the order property, and sort the nodes + * beforehand using the {@link pv.Dom} operator. + * + * @see pv.Dom.Node#sort + * @type string + * @name pv.Layout.Partition.prototype.order + */ + +/** + * The orientation. The default orientation is "top", which means that the root + * node is placed on the top edge, leaf nodes appear at the bottom, and internal + * nodes are in-between. The following orientations are supported:<ul> + * + * <li>left - left-to-right. + * <li>right - right-to-left. + * <li>top - top-to-bottom. + * <li>bottom - bottom-to-top. + * <li>radial - radially, with the root at the center.</ul> + * + * @type string + * @name pv.Layout.Partition.prototype.orient + */ + +/** + * The inner radius; defaults to 0. This property applies only to radial + * orientations, and can be used to compress the layout radially. Note that for + * the node-link implementation, the root node is always at the center, + * regardless of the value of this property; this property only affects internal + * and leaf nodes. For the space-filling implementation, a non-zero value of + * this property will result in the root node represented as a ring rather than + * a circle. + * + * @type number + * @name pv.Layout.Partition.prototype.innerRadius + */ + +/** + * The outer radius; defaults to fill the containing panel, based on the height + * and width of the layout. If the layout has no height and width specified, it + * will extend to fill the enclosing panel. + * + * @type number + * @name pv.Layout.Partition.prototype.outerRadius + */ + +/** + * Default properties for partition layouts. The default orientation is "top". + * + * @type pv.Layout.Partition + */ +pv.Layout.Partition.prototype.defaults = new pv.Layout.Partition() + .extend(pv.Layout.Hierarchy.prototype.defaults) + .orient("top"); + +/** @private */ +pv.Layout.Partition.prototype.$size = function() { return 1; }; + +/** + * Specifies the sizing function. By default, a sizing function is disabled and + * all nodes are given constant size. The sizing function is invoked for each + * leaf node in the tree (passed to the constructor). + * + * <p>For example, if the tree data structure represents a file system, with + * files as leaf nodes, and each file has a <tt>bytes</tt> attribute, you can + * specify a size function as: + * + * <pre> .size(function(d) d.bytes)</pre> + * + * As with other properties, a size function may specify additional arguments to + * access the data associated with the layout and any enclosing panels. + * + * @param {function} f the new sizing function. + * @returns {pv.Layout.Partition} this. + */ +pv.Layout.Partition.prototype.size = function(f) { + this.$size = f; + return this; +}; + +/** @private */ +pv.Layout.Partition.prototype.buildImplied = function(s) { + if (pv.Layout.Hierarchy.prototype.buildImplied.call(this, s)) return; + + var that = this, + root = s.nodes[0], + stack = pv.Mark.stack, + maxDepth = 0; + + /* Recursively compute the tree depth and node size. */ + stack.unshift(null); + root.visitAfter(function(n, i) { + if (i > maxDepth) maxDepth = i; + n.size = n.firstChild + ? pv.sum(n.childNodes, function(n) { return n.size; }) + : that.$size.apply(that, (stack[0] = n, stack)); + }); + stack.shift(); + + /* Order */ + switch (s.order) { + case "ascending": root.sort(function(a, b) { return a.size - b.size; }); break; + case "descending": root.sort(function(b, a) { return a.size - b.size; }); break; + } + + /* Compute the unit breadth and depth of each node. */ + var ds = 1 / maxDepth; + root.minBreadth = 0; + root.breadth = .5; + root.maxBreadth = 1; + root.visitBefore(function(n) { + var b = n.minBreadth, s = n.maxBreadth - b; + for (var c = n.firstChild; c; c = c.nextSibling) { + c.minBreadth = b; + c.maxBreadth = b += (c.size / n.size) * s; + c.breadth = (b + c.minBreadth) / 2; + } + }); + root.visitAfter(function(n, i) { + n.minDepth = (i - 1) * ds; + n.maxDepth = n.depth = i * ds; + }); + + pv.Layout.Hierarchy.NodeLink.buildImplied.call(this, s); +}; + +/** + * Constructs a new, empty space-filling partition layout. Layouts are not + * typically constructed directly; instead, they are added to an existing panel + * via {@link pv.Mark#add}. + * + * @class A variant of partition layout that is space-filling. The meaning of + * the exported mark prototypes changes slightly in the space-filling + * implementation:<ul> + * + * <li><tt>node</tt> - for rendering nodes; typically a {@link pv.Bar} for + * non-radial orientations, and a {@link pv.Wedge} for radial orientations. + * + * <p><li><tt>link</tt> - unsupported; undefined. Links are encoded implicitly + * in the arrangement of the space-filling nodes. + * + * <p><li><tt>label</tt> - for rendering node labels; typically a + * {@link pv.Label}. + * + * </ul>For more details on how to use this layout, see + * {@link pv.Layout.Partition}. + * + * @extends pv.Layout.Partition + */ +pv.Layout.Partition.Fill = function() { + pv.Layout.Partition.call(this); + pv.Layout.Hierarchy.Fill.constructor.call(this); +}; + +pv.Layout.Partition.Fill.prototype = pv.extend(pv.Layout.Partition); + +/** @private */ +pv.Layout.Partition.Fill.prototype.buildImplied = function(s) { + if (pv.Layout.Partition.prototype.buildImplied.call(this, s)) return; + pv.Layout.Hierarchy.Fill.buildImplied.call(this, s); +}; +/** + * Constructs a new, empty arc layout. Layouts are not typically constructed + * directly; instead, they are added to an existing panel via + * {@link pv.Mark#add}. + * + * @class Implements a layout for arc diagrams. An arc diagram is a network + * visualization with a one-dimensional layout of nodes, using circular arcs to + * render links between nodes. For undirected networks, arcs are rendering on a + * single side; this makes arc diagrams useful as annotations to other + * two-dimensional network layouts, such as rollup, matrix or table layouts. For + * directed networks, links in opposite directions can be rendered on opposite + * sides using <tt>directed(true)</tt>. + * + * <p>Arc layouts are particularly sensitive to node ordering; for best results, + * order the nodes such that related nodes are close to each other. A poor + * (e.g., random) order may result in large arcs with crossovers that impede + * visual processing. A future improvement to this layout may include automatic + * reordering using, e.g., spectral graph layout or simulated annealing. + * + * <p>This visualization technique is related to that developed by + * M. Wattenberg, <a + * href="http://www.research.ibm.com/visual/papers/arc-diagrams.pdf">"Arc + * Diagrams: Visualizing Structure in Strings"</a> in <i>IEEE InfoVis</i>, 2002. + * However, this implementation is limited to simple node-link networks, as + * opposed to structures with hierarchical self-similarity (such as strings). + * + * <p>As with other network layouts, three mark prototypes are provided:<ul> + * + * <li><tt>node</tt> - for rendering nodes; typically a {@link pv.Dot}. + * <li><tt>link</tt> - for rendering links; typically a {@link pv.Line}. + * <li><tt>label</tt> - for rendering node labels; typically a {@link pv.Label}. + * + * </ul>For more details on how this layout is structured and can be customized, + * see {@link pv.Layout.Network}. + * + * @extends pv.Layout.Network + **/ +pv.Layout.Arc = function() { + pv.Layout.Network.call(this); + var interpolate, // cached interpolate + directed, // cached directed + reverse, // cached reverse + buildImplied = this.buildImplied; + + /** @private Cache layout state to optimize properties. */ + this.buildImplied = function(s) { + buildImplied.call(this, s); + directed = s.directed; + interpolate = s.orient == "radial" ? "linear" : "polar"; + reverse = s.orient == "right" || s.orient == "top"; + }; + + /* Override link properties to handle directedness and orientation. */ + this.link + .data(function(p) { + var s = p.sourceNode, t = p.targetNode; + return reverse != (directed || (s.breadth < t.breadth)) ? [s, t] : [t, s]; + }) + .interpolate(function() { return interpolate; }); +}; + +pv.Layout.Arc.prototype = pv.extend(pv.Layout.Network) + .property("orient", String) + .property("directed", Boolean); + +/** + * Default properties for arc layouts. By default, the orientation is "bottom". + * + * @type pv.Layout.Arc + */ +pv.Layout.Arc.prototype.defaults = new pv.Layout.Arc() + .extend(pv.Layout.Network.prototype.defaults) + .orient("bottom"); + +/** + * Specifies an optional sort function. The sort function follows the same + * comparator contract required by {@link pv.Dom.Node#sort}. Specifying a sort + * function provides an alternative to sort the nodes as they are specified by + * the <tt>nodes</tt> property; the main advantage of doing this is that the + * comparator function can access implicit fields populated by the network + * layout, such as the <tt>linkDegree</tt>. + * + * <p>Note that arc diagrams are particularly sensitive to order. This is + * referred to as the seriation problem, and many different techniques exist to + * find good node orders that emphasize clusters, such as spectral layout and + * simulated annealing. + * + * @param {function} f comparator function for nodes. + * @returns {pv.Layout.Arc} this. + */ +pv.Layout.Arc.prototype.sort = function(f) { + this.$sort = f; + return this; +}; + +/** @private Populates the x, y and angle attributes on the nodes. */ +pv.Layout.Arc.prototype.buildImplied = function(s) { + if (pv.Layout.Network.prototype.buildImplied.call(this, s)) return; + + var nodes = s.nodes, + orient = s.orient, + sort = this.$sort, + index = pv.range(nodes.length), + w = s.width, + h = s.height, + r = Math.min(w, h) / 2; + + /* Sort the nodes. */ + if (sort) index.sort(function(a, b) { return sort(nodes[a], nodes[b]); }); + + /** @private Returns the mid-angle, given the breadth. */ + function midAngle(b) { + switch (orient) { + case "top": return -Math.PI / 2; + case "bottom": return Math.PI / 2; + case "left": return Math.PI; + case "right": return 0; + case "radial": return (b - .25) * 2 * Math.PI; + } + } + + /** @private Returns the x-position, given the breadth. */ + function x(b) { + switch (orient) { + case "top": + case "bottom": return b * w; + case "left": return 0; + case "right": return w; + case "radial": return w / 2 + r * Math.cos(midAngle(b)); + } + } + + /** @private Returns the y-position, given the breadth. */ + function y(b) { + switch (orient) { + case "top": return 0; + case "bottom": return h; + case "left": + case "right": return b * h; + case "radial": return h / 2 + r * Math.sin(midAngle(b)); + } + } + + /* Populate the x, y and mid-angle attributes. */ + for (var i = 0; i < nodes.length; i++) { + var n = nodes[index[i]], b = n.breadth = (i + .5) / nodes.length; + n.x = x(b); + n.y = y(b); + n.midAngle = midAngle(b); + } +}; + +/** + * The orientation. The default orientation is "left", which means that nodes + * will be positioned from left-to-right in the order they are specified in the + * <tt>nodes</tt> property. The following orientations are supported:<ul> + * + * <li>left - left-to-right. + * <li>right - right-to-left. + * <li>top - top-to-bottom. + * <li>bottom - bottom-to-top. + * <li>radial - radially, starting at 12 o'clock and proceeding clockwise.</ul> + * + * @type string + * @name pv.Layout.Arc.prototype.orient + */ + +/** + * Whether this arc digram is directed (bidirectional); only applies to + * non-radial orientations. By default, arc digrams are undirected, such that + * all arcs appear on one side. If the arc digram is directed, then forward + * links are drawn on the conventional side (the same as as undirected + * links--right, left, bottom and top for left, right, top and bottom, + * respectively), while reverse links are drawn on the opposite side. + * + * @type boolean + * @name pv.Layout.Arc.prototype.directed + */ +/** + * Constructs a new, empty horizon layout. Layouts are not typically constructed + * directly; instead, they are added to an existing panel via + * {@link pv.Mark#add}. + * + * @class Implements a horizon layout, which is a variation of a single-series + * area chart where the area is folded into multiple bands. Color is used to + * encode band, allowing the size of the chart to be reduced significantly + * without impeding readability. This layout algorithm is based on the work of + * J. Heer, N. Kong and M. Agrawala in <a + * href="http://hci.stanford.edu/publications/2009/heer-horizon-chi09.pdf">"Sizing + * the Horizon: The Effects of Chart Size and Layering on the Graphical + * Perception of Time Series Visualizations"</a>, CHI 2009. + * + * <p>This layout exports a single <tt>band</tt> mark prototype, which is + * intended to be used with an area mark. The band mark is contained in a panel + * which is replicated per band (and for negative/positive bands). For example, + * to create a simple horizon graph given an array of numbers: + * + * <pre>vis.add(pv.Layout.Horizon) + * .bands(n) + * .band.add(pv.Area) + * .data(data) + * .left(function() this.index * 35) + * .height(function(d) d * 40);</pre> + * + * The layout can be further customized by changing the number of bands, and + * toggling whether the negative bands are mirrored or offset. (See the + * above-referenced paper for guidance.) + * + * <p>The <tt>fillStyle</tt> of the area can be overridden, though typically it + * is easier to customize the layout's behavior through the custom + * <tt>backgroundStyle</tt>, <tt>positiveStyle</tt> and <tt>negativeStyle</tt> + * properties. By default, the background is white, positive bands are blue, and + * negative bands are red. For the most accurate presentation, use fully-opaque + * colors of equal intensity for the negative and positive bands. + * + * @extends pv.Layout + */ +pv.Layout.Horizon = function() { + pv.Layout.call(this); + var that = this, + bands, // cached bands + mode, // cached mode + size, // cached height + fill, // cached background style + red, // cached negative color (ramp) + blue, // cached positive color (ramp) + buildImplied = this.buildImplied; + + /** @private Cache the layout state to optimize properties. */ + this.buildImplied = function(s) { + buildImplied.call(this, s); + bands = s.bands; + mode = s.mode; + size = Math.round((mode == "color" ? .5 : 1) * s.height); + fill = s.backgroundStyle; + red = pv.ramp(fill, s.negativeStyle).domain(0, bands); + blue = pv.ramp(fill, s.positiveStyle).domain(0, bands); + }; + + var bands = new pv.Panel() + .data(function() { return pv.range(bands * 2); }) + .overflow("hidden") + .height(function() { return size; }) + .top(function(i) { return mode == "color" ? (i & 1) * size : 0; }) + .fillStyle(function(i) { return i ? null : fill; }); + + /** + * The band prototype. This prototype is intended to be used with an Area + * mark to render the horizon bands. + * + * @type pv.Mark + * @name pv.Layout.Horizon.prototype.band + */ + this.band = new pv.Mark() + .top(function(d, i) { + return mode == "mirror" && i & 1 + ? (i + 1 >> 1) * size + : null; + }) + .bottom(function(d, i) { + return mode == "mirror" + ? (i & 1 ? null : (i + 1 >> 1) * -size) + : ((i & 1 || -1) * (i + 1 >> 1) * size); + }) + .fillStyle(function(d, i) { + return (i & 1 ? red : blue)((i >> 1) + 1); + }); + + this.band.add = function(type) { + return that.add(pv.Panel).extend(bands).add(type).extend(this); + }; +}; + +pv.Layout.Horizon.prototype = pv.extend(pv.Layout) + .property("bands", Number) + .property("mode", String) + .property("backgroundStyle", pv.color) + .property("positiveStyle", pv.color) + .property("negativeStyle", pv.color); + +/** + * Default properties for horizon layouts. By default, there are two bands, the + * mode is "offset", the background style is "white", the positive style is + * blue, negative style is red. + * + * @type pv.Layout.Horizon + */ +pv.Layout.Horizon.prototype.defaults = new pv.Layout.Horizon() + .extend(pv.Layout.prototype.defaults) + .bands(2) + .mode("offset") + .backgroundStyle("white") + .positiveStyle("#1f77b4") + .negativeStyle("#d62728"); + +/** + * The horizon mode: offset, mirror, or color. The default is "offset". + * + * @type string + * @name pv.Layout.Horizon.prototype.mode + */ + +/** + * The number of bands. Must be at least one. The default value is two. + * + * @type number + * @name pv.Layout.Horizon.prototype.bands + */ + +/** + * The positive band color; if non-null, the interior of positive bands are + * filled with the specified color. The default value of this property is blue. + * For accurate blending, this color should be fully opaque. + * + * @type pv.Color + * @name pv.Layout.Horizon.prototype.positiveStyle + */ + +/** + * The negative band color; if non-null, the interior of negative bands are + * filled with the specified color. The default value of this property is red. + * For accurate blending, this color should be fully opaque. + * + * @type pv.Color + * @name pv.Layout.Horizon.prototype.negativeStyle + */ + +/** + * The background color. The panel background is filled with the specified + * color, and the negative and positive bands are filled with an interpolated + * color between this color and the respective band color. The default value of + * this property is white. For accurate blending, this color should be fully + * opaque. + * + * @type pv.Color + * @name pv.Layout.Horizon.prototype.backgroundStyle + */ +/** + * Constructs a new, empty rollup network layout. Layouts are not typically + * constructed directly; instead, they are added to an existing panel via + * {@link pv.Mark#add}. + * + * @class Implements a network visualization using a node-link diagram where + * nodes are rolled up along two dimensions. This implementation is based on the + * "PivotGraph" designed by Martin Wattenberg: + * + * <blockquote>The method is designed for graphs that are "multivariate", i.e., + * where each node is associated with several attributes. Unlike visualizations + * which emphasize global graph topology, PivotGraph uses a simple grid-based + * approach to focus on the relationship between node attributes & + * connections.</blockquote> + * + * This layout requires two psuedo-properties to be specified, which assign node + * positions along the two dimensions {@link #x} and {@link #y}, corresponding + * to the left and top properties, respectively. Typically, these functions are + * specified using an {@link pv.Scale.ordinal}. Nodes that share the same + * position in <i>x</i> and <i>y</i> are "rolled up" into a meta-node, and + * similarly links are aggregated between meta-nodes. For example, to construct + * a rollup to analyze links by gender and affiliation, first define two ordinal + * scales: + * + * <pre>var x = pv.Scale.ordinal(nodes, function(d) d.gender).split(0, w), + * y = pv.Scale.ordinal(nodes, function(d) d.aff).split(0, h);</pre> + * + * Next, define the position psuedo-properties: + * + * <pre> .x(function(d) x(d.gender)) + * .y(function(d) y(d.aff))</pre> + * + * Linear and other quantitative scales can alternatively be used to position + * the nodes along either dimension. Note, however, that the rollup requires + * that the positions match exactly, and thus ordinal scales are recommended to + * avoid precision errors. + * + * <p>Note that because this layout provides a visualization of the rolled up + * graph, the data properties for the mark prototypes (<tt>node</tt>, + * <tt>link</tt> and <tt>label</tt>) are different from most other network + * layouts: they reference the rolled-up nodes and links, rather than the nodes + * and links of the full network. The underlying nodes and links for each + * rolled-up node and link can be accessed via the <tt>nodes</tt> and + * <tt>links</tt> attributes, respectively. The aggregated link values for + * rolled-up links can similarly be accessed via the <tt>linkValue</tt> + * attribute. + * + * <p>For undirected networks, links are duplicated in both directions. For + * directed networks, use <tt>directed(true)</tt>. The graph is assumed to be + * undirected by default. + * + * @extends pv.Layout.Network + * @see <a href="http://www.research.ibm.com/visual/papers/pivotgraph.pdf" + * >"Visual Exploration of Multivariate Graphs"</a> by M. Wattenberg, CHI 2006. + */ +pv.Layout.Rollup = function() { + pv.Layout.Network.call(this); + var that = this, + nodes, // cached rollup nodes + links, // cached rollup links + buildImplied = that.buildImplied; + + /** @private Cache layout state to optimize properties. */ + this.buildImplied = function(s) { + buildImplied.call(this, s); + nodes = s.$rollup.nodes; + links = s.$rollup.links; + }; + + /* Render rollup nodes. */ + this.node + .data(function() { return nodes; }) + .size(function(d) { return d.nodes.length * 20; }); + + /* Render rollup links. */ + this.link + .interpolate("polar") + .eccentricity(.8); + + this.link.add = function(type) { + return that.add(pv.Panel) + .data(function() { return links; }) + .add(type) + .extend(this); + }; +}; + +pv.Layout.Rollup.prototype = pv.extend(pv.Layout.Network) + .property("directed", Boolean); + +/** + * Whether the underlying network is directed. By default, the graph is assumed + * to be undirected, and links are rendered in both directions. If the network + * is directed, then forward links are drawn above the diagonal, while reverse + * links are drawn below. + * + * @type boolean + * @name pv.Layout.Rollup.prototype.directed + */ + +/** + * Specifies the <i>x</i>-position function used to rollup nodes. The rolled up + * nodes are positioned horizontally using the return values from the given + * function. Typically the function is specified as an ordinal scale. For + * single-dimension rollups, a constant value can be specified. + * + * @param {function} f the <i>x</i>-position function. + * @returns {pv.Layout.Rollup} this. + * @see pv.Scale.ordinal + */ +pv.Layout.Rollup.prototype.x = function(f) { + this.$x = pv.functor(f); + return this; +}; + +/** + * Specifies the <i>y</i>-position function used to rollup nodes. The rolled up + * nodes are positioned vertically using the return values from the given + * function. Typically the function is specified as an ordinal scale. For + * single-dimension rollups, a constant value can be specified. + * + * @param {function} f the <i>y</i>-position function. + * @returns {pv.Layout.Rollup} this. + * @see pv.Scale.ordinal + */ +pv.Layout.Rollup.prototype.y = function(f) { + this.$y = pv.functor(f); + return this; +}; + +/** @private */ +pv.Layout.Rollup.prototype.buildImplied = function(s) { + if (pv.Layout.Network.prototype.buildImplied.call(this, s)) return; + + var nodes = s.nodes, + links = s.links, + directed = s.directed, + n = nodes.length, + x = [], + y = [], + rnindex = 0, + rnodes = {}, + rlinks = {}; + + /** @private */ + function id(i) { + return x[i] + "," + y[i]; + } + + /* Iterate over the data, evaluating the x and y functions. */ + var stack = pv.Mark.stack, o = {parent: this}; + stack.unshift(null); + for (var i = 0; i < n; i++) { + o.index = i; + stack[0] = nodes[i]; + x[i] = this.$x.apply(o, stack); + y[i] = this.$y.apply(o, stack); + } + stack.shift(); + + /* Compute rollup nodes. */ + for (var i = 0; i < nodes.length; i++) { + var nodeId = id(i), + rn = rnodes[nodeId]; + if (!rn) { + rn = rnodes[nodeId] = pv.extend(nodes[i]); + rn.index = rnindex++; + rn.x = x[i]; + rn.y = y[i]; + rn.nodes = []; + } + rn.nodes.push(nodes[i]); + } + + /* Compute rollup links. */ + for (var i = 0; i < links.length; i++) { + var source = links[i].sourceNode, + target = links[i].targetNode, + rsource = rnodes[id(source.index)], + rtarget = rnodes[id(target.index)], + reverse = !directed && rsource.index > rtarget.index, + linkId = reverse + ? rtarget.index + "," + rsource.index + : rsource.index + "," + rtarget.index, + rl = rlinks[linkId]; + if (!rl) { + rl = rlinks[linkId] = { + sourceNode: rsource, + targetNode: rtarget, + linkValue: 0, + links: [] + }; + } + rl.links.push(links[i]); + rl.linkValue += links[i].linkValue; + } + + /* Export the rolled up nodes and links to the scene. */ + s.$rollup = { + nodes: pv.values(rnodes), + links: pv.values(rlinks) + }; +}; +/** + * Constructs a new, empty matrix network layout. Layouts are not typically + * constructed directly; instead, they are added to an existing panel via + * {@link pv.Mark#add}. + * + * @class Implements a network visualization using a matrix view. This is, in + * effect, a visualization of the graph's <i>adjacency matrix</i>: the cell at + * row <i>i</i>, column <i>j</i>, corresponds to the link from node <i>i</i> to + * node <i>j</i>. The fill color of each cell is binary by default, and + * corresponds to whether a link exists between the two nodes. If the underlying + * graph has links with variable values, the <tt>fillStyle</tt> property can be + * substited to use an appropriate color function, such as {@link pv.ramp}. + * + * <p>For undirected networks, the matrix is symmetric around the diagonal. For + * directed networks, links in opposite directions can be rendered on opposite + * sides of the diagonal using <tt>directed(true)</tt>. The graph is assumed to + * be undirected by default. + * + * <p>The mark prototypes for this network layout are slightly different than + * other implementations:<ul> + * + * <li><tt>node</tt> - unsupported; undefined. No mark is needed to visualize + * nodes directly, as the nodes are implicit in the location (rows and columns) + * of the links. + * + * <p><li><tt>link</tt> - for rendering links; typically a {@link pv.Bar}. The + * link mark is added directly to the layout, with the data property defined as + * all possible pairs of nodes. Each pair is represented as a + * {@link pv.Network.Layout.Link}, though the <tt>linkValue</tt> attribute may + * be 0 if no link exists in the graph. + * + * <p><li><tt>label</tt> - for rendering node labels; typically a + * {@link pv.Label}. The label mark is added directly to the layout, with the + * data property defined via the layout's <tt>nodes</tt> property; note, + * however, that the nodes are duplicated so as to provide a label across the + * top and down the side. Properties such as <tt>strokeStyle</tt> and + * <tt>fillStyle</tt> can be overridden to compute properties from node data + * dynamically. + * + * </ul>For more details on how to use this layout, see + * {@link pv.Layout.Network}. + * + * @extends pv.Layout.Network + */ +pv.Layout.Matrix = function() { + pv.Layout.Network.call(this); + var that = this, + n, // cached matrix size + dx, // cached cell width + dy, // cached cell height + labels, // cached labels (array of strings) + pairs, // cached pairs (array of links) + buildImplied = that.buildImplied; + + /** @private Cache layout state to optimize properties. */ + this.buildImplied = function(s) { + buildImplied.call(this, s); + n = s.nodes.length; + dx = s.width / n; + dy = s.height / n; + labels = s.$matrix.labels; + pairs = s.$matrix.pairs; + }; + + /* Links are all pairs of nodes. */ + this.link + .data(function() { return pairs; }) + .left(function() { return dx * (this.index % n); }) + .top(function() { return dy * Math.floor(this.index / n); }) + .width(function() { return dx; }) + .height(function() { return dy; }) + .lineWidth(1.5) + .strokeStyle("#fff") + .fillStyle(function(l) { return l.linkValue ? "#555" : "#eee"; }) + .parent = this; + + /* No special add for links! */ + delete this.link.add; + + /* Labels are duplicated for top & left. */ + this.label + .data(function() { return labels; }) + .left(function() { return this.index & 1 ? dx * ((this.index >> 1) + .5) : 0; }) + .top(function() { return this.index & 1 ? 0 : dy * ((this.index >> 1) + .5); }) + .textMargin(4) + .textAlign(function() { return this.index & 1 ? "left" : "right"; }) + .textAngle(function() { return this.index & 1 ? -Math.PI / 2 : 0; }); + + /* The node mark is unused. */ + delete this.node; +}; + +pv.Layout.Matrix.prototype = pv.extend(pv.Layout.Network) + .property("directed", Boolean); + +/** + * Whether this matrix visualization is directed (bidirectional). By default, + * the graph is assumed to be undirected, such that the visualization is + * symmetric across the matrix diagonal. If the network is directed, then + * forward links are drawn above the diagonal, while reverse links are drawn + * below. + * + * @type boolean + * @name pv.Layout.Matrix.prototype.directed + */ + +/** + * Specifies an optional sort function. The sort function follows the same + * comparator contract required by {@link pv.Dom.Node#sort}. Specifying a sort + * function provides an alternative to sort the nodes as they are specified by + * the <tt>nodes</tt> property; the main advantage of doing this is that the + * comparator function can access implicit fields populated by the network + * layout, such as the <tt>linkDegree</tt>. + * + * <p>Note that matrix visualizations are particularly sensitive to order. This + * is referred to as the seriation problem, and many different techniques exist + * to find good node orders that emphasize clusters, such as spectral layout and + * simulated annealing. + * + * @param {function} f comparator function for nodes. + * @returns {pv.Layout.Matrix} this. + */ +pv.Layout.Matrix.prototype.sort = function(f) { + this.$sort = f; + return this; +}; + +/** @private */ +pv.Layout.Matrix.prototype.buildImplied = function(s) { + if (pv.Layout.Network.prototype.buildImplied.call(this, s)) return; + + var nodes = s.nodes, + links = s.links, + sort = this.$sort, + n = nodes.length, + index = pv.range(n), + labels = [], + pairs = [], + map = {}; + + s.$matrix = {labels: labels, pairs: pairs}; + + /* Sort the nodes. */ + if (sort) index.sort(function(a, b) { return sort(nodes[a], nodes[b]); }); + + /* Create pairs. */ + for (var i = 0; i < n; i++) { + for (var j = 0; j < n; j++) { + var a = index[i], + b = index[j], + p = { + row: i, + col: j, + sourceNode: nodes[a], + targetNode: nodes[b], + linkValue: 0 + }; + pairs.push(map[a + "." + b] = p); + } + } + + /* Create labels. */ + for (var i = 0; i < n; i++) { + var a = index[i]; + labels.push(nodes[a], nodes[a]); + } + + /* Accumulate link values. */ + for (var i = 0; i < links.length; i++) { + var l = links[i], + source = l.sourceNode.index, + target = l.targetNode.index, + value = l.linkValue; + map[source + "." + target].linkValue += value; + if (!s.directed) map[target + "." + source].linkValue += value; + } +}; +// ranges (bad, satisfactory, good) +// measures (actual, forecast) +// markers (previous, goal) + +/* + * Chart design based on the recommendations of Stephen Few. Implementation + * based on the work of Clint Ivy, Jamie Love, and Jason Davies. + * http://projects.instantcognition.com/protovis/bulletchart/ + */ + +/** + * Constructs a new, empty bullet layout. Layouts are not typically constructed + * directly; instead, they are added to an existing panel via + * {@link pv.Mark#add}. + * + * @class + * @extends pv.Layout + */ +pv.Layout.Bullet = function() { + pv.Layout.call(this); + var that = this, + buildImplied = that.buildImplied, + scale = that.x = pv.Scale.linear(), + orient, + horizontal, + rangeColor, + measureColor, + x; + + /** @private Cache layout state to optimize properties. */ + this.buildImplied = function(s) { + buildImplied.call(this, x = s); + orient = s.orient; + horizontal = /^left|right$/.test(orient); + rangeColor = pv.ramp("#bbb", "#eee") + .domain(0, Math.max(1, x.ranges.length - 1)); + measureColor = pv.ramp("steelblue", "lightsteelblue") + .domain(0, Math.max(1, x.measures.length - 1)); + }; + + /** + * The range prototype. + * + * @type pv.Mark + * @name pv.Layout.Bullet.prototype.range + */ + (this.range = new pv.Mark()) + .data(function() { return x.ranges; }) + .reverse(true) + .left(function() { return orient == "left" ? 0 : null; }) + .top(function() { return orient == "top" ? 0 : null; }) + .right(function() { return orient == "right" ? 0 : null; }) + .bottom(function() { return orient == "bottom" ? 0 : null; }) + .width(function(d) { return horizontal ? scale(d) : null; }) + .height(function(d) { return horizontal ? null : scale(d); }) + .fillStyle(function() { return rangeColor(this.index); }) + .antialias(false) + .parent = that; + + /** + * The measure prototype. + * + * @type pv.Mark + * @name pv.Layout.Bullet.prototype.measure + */ + (this.measure = new pv.Mark()) + .extend(this.range) + .data(function() { return x.measures; }) + .left(function() { return orient == "left" ? 0 : horizontal ? null : this.parent.width() / 3.25; }) + .top(function() { return orient == "top" ? 0 : horizontal ? this.parent.height() / 3.25 : null; }) + .right(function() { return orient == "right" ? 0 : horizontal ? null : this.parent.width() / 3.25; }) + .bottom(function() { return orient == "bottom" ? 0 : horizontal ? this.parent.height() / 3.25 : null; }) + .fillStyle(function() { return measureColor(this.index); }) + .parent = that; + + /** + * The marker prototype. + * + * @type pv.Mark + * @name pv.Layout.Bullet.prototype.marker + */ + (this.marker = new pv.Mark()) + .data(function() { return x.markers; }) + .left(function(d) { return orient == "left" ? scale(d) : horizontal ? null : this.parent.width() / 2; }) + .top(function(d) { return orient == "top" ? scale(d) : horizontal ? this.parent.height() / 2 : null; }) + .right(function(d) { return orient == "right" ? scale(d) : null; }) + .bottom(function(d) { return orient == "bottom" ? scale(d) : null; }) + .strokeStyle("black") + .shape("bar") + .angle(function() { return horizontal ? 0 : Math.PI / 2; }) + .parent = that; + + (this.tick = new pv.Mark()) + .data(function() { return scale.ticks(7); }) + .left(function(d) { return orient == "left" ? scale(d) : null; }) + .top(function(d) { return orient == "top" ? scale(d) : null; }) + .right(function(d) { return orient == "right" ? scale(d) : horizontal ? null : -6; }) + .bottom(function(d) { return orient == "bottom" ? scale(d) : horizontal ? -8 : null; }) + .height(function() { return horizontal ? 6 : null; }) + .width(function() { return horizontal ? null : 6; }) + .parent = that; +}; + +pv.Layout.Bullet.prototype = pv.extend(pv.Layout) + .property("orient", String) // left, right, top, bottom + .property("ranges") + .property("markers") + .property("measures") + .property("maximum", Number); + +/** + * Default properties for bullet layouts. + * + * @type pv.Layout.Bullet + */ +pv.Layout.Bullet.prototype.defaults = new pv.Layout.Bullet() + .extend(pv.Layout.prototype.defaults) + .orient("left") + .ranges([]) + .markers([]) + .measures([]); + +/** + * The orientation. + * + * @type string + * @name pv.Layout.Bullet.prototype.orient + */ + +/** + * The array of range values. + * + * @type array + * @name pv.Layout.Bullet.prototype.ranges + */ + +/** + * The array of marker values. + * + * @type array + * @name pv.Layout.Bullet.prototype.markers + */ + +/** + * The array of measure values. + * + * @type array + * @name pv.Layout.Bullet.prototype.measures + */ + +/** + * Optional; the maximum range value. + * + * @type number + * @name pv.Layout.Bullet.prototype.maximum + */ + +/** @private */ +pv.Layout.Bullet.prototype.buildImplied = function(s) { + pv.Layout.prototype.buildImplied.call(this, s); + var size = this.parent[/^left|right$/.test(s.orient) ? "width" : "height"](); + s.maximum = s.maximum || pv.max([].concat(s.ranges, s.markers, s.measures)); + this.x.domain(0, s.maximum).range(0, size); +}; +/** + * Abstract; see an implementing class for details. + * + * @class Represents a reusable interaction; applies an interactive behavior to + * a given mark. Behaviors are themselves functions designed to be used as event + * handlers. For example, to add pan and zoom support to any panel, say: + * + * <pre> .event("mousedown", pv.Behavior.pan()) + * .event("mousewheel", pv.Behavior.zoom())</pre> + * + * The behavior should be registered on the event that triggers the start of the + * behavior. Typically, the behavior will take care of registering for any + * additional events that are necessary. For example, dragging starts on + * mousedown, while the drag behavior automatically listens for mousemove and + * mouseup events on the window. By listening to the window, the behavior can + * continue to receive mouse events even if the mouse briefly leaves the mark + * being dragged, or even the root panel. + * + * <p>Each behavior implementation has specific requirements as to which events + * it supports, and how it should be used. For example, the drag behavior + * requires that the data associated with the mark be an object with <tt>x</tt> + * and <tt>y</tt> attributes, such as a {@link pv.Vector}, storing the mark's + * position. See an implementing class for details. + * + * @see pv.Behavior.drag + * @see pv.Behavior.pan + * @see pv.Behavior.point + * @see pv.Behavior.select + * @see pv.Behavior.zoom + * @extends function + */ +pv.Behavior = {}; +/** + * Returns a new drag behavior to be registered on mousedown events. + * + * @class Implements interactive dragging starting with mousedown events. + * Register this behavior on marks that should be draggable by the user, such as + * the selected region for brushing and linking. This behavior can be used in + * tandom with {@link pv.Behavior.select} to allow the selected region to be + * dragged interactively. + * + * <p>After the initial mousedown event is triggered, this behavior listens for + * mousemove and mouseup events on the window. This allows dragging to continue + * even if the mouse temporarily leaves the mark that is being dragged, or even + * the root panel. + * + * <p>This behavior requires that the data associated with the mark being + * dragged have <tt>x</tt> and <tt>y</tt> attributes that correspond to the + * mark's location in pixels. The mark's positional properties are not set + * directly by this behavior; instead, the positional properties should be + * defined as: + * + * <pre> .left(function(d) d.x) + * .top(function(d) d.y)</pre> + * + * Thus, the behavior does not move the mark directly, but instead updates the + * mark position by updating the underlying data. Note that if the positional + * properties are defined with bottom and right (rather than top and left), the + * drag behavior will be inverted, which will confuse users! + * + * <p>The drag behavior is bounded by the parent panel; the <tt>x</tt> and + * <tt>y</tt> attributes are clamped such that the mark being dragged does not + * extend outside the enclosing panel's bounds. To facilitate this, the drag + * behavior also queries for <tt>dx</tt> and <tt>dy</tt> attributes on the + * underlying data, to determine the dimensions of the bar being dragged. For + * non-rectangular marks, the drag behavior simply treats the mark as a point, + * which means that only the mark's center is bounded. + * + * <p>The mark being dragged is automatically re-rendered for each mouse event + * as part of the drag operation. In addition, a <tt>fix</tt> attribute is + * populated on the mark, which allows visual feedback for dragging. For + * example, to change the mark fill color while dragging: + * + * <pre> .fillStyle(function(d) d.fix ? "#ff7f0e" : "#aec7e8")</pre> + * + * In some cases, such as with network layouts, dragging the mark may cause + * related marks to change, in which case additional marks may also need to be + * rendered. This can be accomplished by listening for the drag + * psuedo-events:<ul> + * + * <li>dragstart (on mousedown) + * <li>drag (on mousemove) + * <li>dragend (on mouseup) + * + * </ul>For example, to render the parent panel while dragging, thus + * re-rendering all sibling marks: + * + * <pre> .event("mousedown", pv.Behavior.drag()) + * .event("drag", function() this.parent)</pre> + * + * This behavior may be enhanced in the future to allow more flexible + * configuration of drag behavior. + * + * @extends pv.Behavior + * @see pv.Behavior + * @see pv.Behavior.select + * @see pv.Layout.force + */ +pv.Behavior.drag = function() { + var scene, // scene context + index, // scene context + p, // particle being dragged + v1, // initial mouse-particle offset + max; + + /** @private */ + function mousedown(d) { + index = this.index; + scene = this.scene; + var m = this.mouse(); + v1 = ((p = d).fix = pv.vector(d.x, d.y)).minus(m); + max = { + x: this.parent.width() - (d.dx || 0), + y: this.parent.height() - (d.dy || 0) + }; + scene.mark.context(scene, index, function() { this.render(); }); + pv.Mark.dispatch("dragstart", scene, index); + } + + /** @private */ + function mousemove() { + if (!scene) return; + scene.mark.context(scene, index, function() { + var m = this.mouse(); + p.x = p.fix.x = Math.max(0, Math.min(v1.x + m.x, max.x)); + p.y = p.fix.y = Math.max(0, Math.min(v1.y + m.y, max.y)); + this.render(); + }); + pv.Mark.dispatch("drag", scene, index); + } + + /** @private */ + function mouseup() { + if (!scene) return; + p.fix = null; + scene.mark.context(scene, index, function() { this.render(); }); + pv.Mark.dispatch("dragend", scene, index); + scene = null; + } + + pv.listen(window, "mousemove", mousemove); + pv.listen(window, "mouseup", mouseup); + return mousedown; +}; +/** + * Returns a new point behavior to be registered on mousemove events. + * + * @class Implements interactive fuzzy pointing, identifying marks that are in + * close proximity to the mouse cursor. This behavior is an alternative to the + * native mouseover and mouseout events, improving usability. Rather than + * requiring the user to mouseover a mark exactly, the mouse simply needs to + * move near the given mark and a "point" event is triggered. In addition, if + * multiple marks overlap, the point behavior can be used to identify the mark + * instance closest to the cursor, as opposed to the one that is rendered on + * top. + * + * <p>The point behavior can also identify the closest mark instance for marks + * that produce a continuous graphic primitive. The point behavior can thus be + * used to provide details-on-demand for both discrete marks (such as dots and + * bars), as well as continuous marks (such as lines and areas). + * + * <p>This behavior is implemented by finding the closest mark instance to the + * mouse cursor on every mousemove event. If this closest mark is within the + * given radius threshold, which defaults to 30 pixels, a "point" psuedo-event + * is dispatched to the given mark instance. If any mark were previously + * pointed, it would receive a corresponding "unpoint" event. These two + * psuedo-event types correspond to the native "mouseover" and "mouseout" + * events, respectively. To increase the radius at which the point behavior can + * be applied, specify an appropriate threshold to the constructor, up to + * <tt>Infinity</tt>. + * + * <p>By default, the standard Cartesian distance is computed. However, with + * some visualizations it is desirable to consider only a single dimension, such + * as the <i>x</i>-dimension for an independent variable. In this case, the + * collapse parameter can be set to collapse the <i>y</i> dimension: + * + * <pre> .event("mousemove", pv.Behavior.point(Infinity).collapse("y"))</pre> + * + * <p>This behavior only listens to mousemove events on the assigned panel, + * which is typically the root panel. The behavior will search recursively for + * descendant marks to point. If the mouse leaves the assigned panel, the + * behavior no longer receives mousemove events; an unpoint psuedo-event is + * automatically dispatched to unpoint any pointed mark. Marks may be re-pointed + * when the mouse reenters the panel. + * + * <p>Panels have transparent fill styles by default; this means that panels may + * not receive the initial mousemove event to start pointing. To fix this + * problem, either given the panel a visible fill style (such as "white"), or + * set the <tt>events</tt> property to "all" such that the panel receives events + * despite its transparent fill. + * + * <p>Note: this behavior does not currently wedge marks. + * + * @extends pv.Behavior + * + * @param {number} [r] the fuzzy radius threshold in pixels + * @see <a href="http://www.tovigrossman.com/papers/chi2005bubblecursor.pdf" + * >"The Bubble Cursor: Enhancing Target Acquisition by Dynamic Resizing of the + * Cursor's Activation Area"</a> by T. Grossman & R. Balakrishnan, CHI 2005. + */ +pv.Behavior.point = function(r) { + var unpoint, // the current pointer target + collapse = null, // dimensions to collapse + kx = 1, // x-dimension cost scale + ky = 1, // y-dimension cost scale + r2 = arguments.length ? r * r : 900; // fuzzy radius + + /** @private Search for the mark closest to the mouse. */ + function search(scene, index) { + var s = scene[index], + point = {cost: Infinity}; + for (var i = 0, n = s.visible && s.children.length; i < n; i++) { + var child = s.children[i], mark = child.mark, p; + if (mark.type == "panel") { + mark.scene = child; + for (var j = 0, m = child.length; j < m; j++) { + mark.index = j; + p = search(child, j); + if (p.cost < point.cost) point = p; + } + delete mark.scene; + delete mark.index; + } else if (mark.$handlers.point) { + var v = mark.mouse(); + for (var j = 0, m = child.length; j < m; j++) { + var c = child[j], + dx = v.x - c.left - (c.width || 0) / 2, + dy = v.y - c.top - (c.height || 0) / 2, + dd = kx * dx * dx + ky * dy * dy; + if (dd < point.cost) { + point.distance = dx * dx + dy * dy; + point.cost = dd; + point.scene = child; + point.index = j; + } + } + } + } + return point; + } + + /** @private */ + function mousemove() { + /* If the closest mark is far away, clear the current target. */ + var point = search(this.scene, this.index); + if ((point.cost == Infinity) || (point.distance > r2)) point = null; + + /* Unpoint the old target, if it's not the new target. */ + if (unpoint) { + if (point + && (unpoint.scene == point.scene) + && (unpoint.index == point.index)) return; + pv.Mark.dispatch("unpoint", unpoint.scene, unpoint.index); + } + + /* Point the new target, if there is one. */ + if (unpoint = point) { + pv.Mark.dispatch("point", point.scene, point.index); + + /* Unpoint when the mouse leaves the root panel. */ + pv.listen(this.root.canvas(), "mouseout", mouseout); + } + } + + /** @private */ + function mouseout(e) { + if (unpoint && !pv.ancestor(this, e.relatedTarget)) { + pv.Mark.dispatch("unpoint", unpoint.scene, unpoint.index); + unpoint = null; + } + } + + /** + * Sets or gets the collapse parameter. By default, the standard Cartesian + * distance is computed. However, with some visualizations it is desirable to + * consider only a single dimension, such as the <i>x</i>-dimension for an + * independent variable. In this case, the collapse parameter can be set to + * collapse the <i>y</i> dimension: + * + * <pre> .event("mousemove", pv.Behavior.point(Infinity).collapse("y"))</pre> + * + * @function + * @returns {pv.Behavior.point} this, or the current collapse parameter. + * @name pv.Behavior.point.prototype.collapse + * @param {string} [x] the new collapse parameter + */ + mousemove.collapse = function(x) { + if (arguments.length) { + collapse = String(x); + switch (collapse) { + case "y": kx = 1; ky = 0; break; + case "x": kx = 0; ky = 1; break; + default: kx = 1; ky = 1; break; + } + return mousemove; + } + return collapse; + }; + + return mousemove; +}; +/** + * Returns a new select behavior to be registered on mousedown events. + * + * @class Implements interactive selecting starting with mousedown events. + * Register this behavior on panels that should be selectable by the user, such + * for brushing and linking. This behavior can be used in tandom with + * {@link pv.Behavior.drag} to allow the selected region to be dragged + * interactively. + * + * <p>After the initial mousedown event is triggered, this behavior listens for + * mousemove and mouseup events on the window. This allows selecting to continue + * even if the mouse temporarily leaves the assigned panel, or even the root + * panel. + * + * <p>This behavior requires that the data associated with the mark being + * dragged have <tt>x</tt>, <tt>y</tt>, <tt>dx</tt> and <tt>dy</tt> attributes + * that correspond to the mark's location and dimensions in pixels. The mark's + * positional properties are not set directly by this behavior; instead, the + * positional properties should be defined as: + * + * <pre> .left(function(d) d.x) + * .top(function(d) d.y) + * .width(function(d) d.dx) + * .height(function(d) d.dy)</pre> + * + * Thus, the behavior does not resize the mark directly, but instead updates the + * selection by updating the assigned panel's underlying data. Note that if the + * positional properties are defined with bottom and right (rather than top and + * left), the drag behavior will be inverted, which will confuse users! + * + * <p>The select behavior is bounded by the assigned panel; the positional + * attributes are clamped such that the selection does not extend outside the + * panel's bounds. + * + * <p>The panel being selected is automatically re-rendered for each mouse event + * as part of the drag operation. This behavior may be enhanced in the future to + * allow more flexible configuration of select behavior. In some cases, such as + * with parallel coordinates, making a selection may cause related marks to + * change, in which case additional marks may also need to be rendered. This can + * be accomplished by listening for the select psuedo-events:<ul> + * + * <li>selectstart (on mousedown) + * <li>select (on mousemove) + * <li>selectend (on mouseup) + * + * </ul>For example, to render the parent panel while selecting, thus + * re-rendering all sibling marks: + * + * <pre> .event("mousedown", pv.Behavior.drag()) + * .event("select", function() this.parent)</pre> + * + * This behavior may be enhanced in the future to allow more flexible + * configuration of the selection behavior. + * + * @extends pv.Behavior + * @see pv.Behavior.drag + */ +pv.Behavior.select = function() { + var scene, // scene context + index, // scene context + r, // region being selected + m1; // initial mouse position + + /** @private */ + function mousedown(d) { + index = this.index; + scene = this.scene; + m1 = this.mouse(); + r = d; + r.x = m1.x; + r.y = m1.y; + r.dx = r.dy = 0; + pv.Mark.dispatch("selectstart", scene, index); + } + + /** @private */ + function mousemove() { + if (!scene) return; + scene.mark.context(scene, index, function() { + var m2 = this.mouse(); + r.x = Math.max(0, Math.min(m1.x, m2.x)); + r.y = Math.max(0, Math.min(m1.y, m2.y)); + r.dx = Math.min(this.width(), Math.max(m2.x, m1.x)) - r.x; + r.dy = Math.min(this.height(), Math.max(m2.y, m1.y)) - r.y; + this.render(); + }); + pv.Mark.dispatch("select", scene, index); + } + + /** @private */ + function mouseup() { + if (!scene) return; + pv.Mark.dispatch("selectend", scene, index); + scene = null; + } + + pv.listen(window, "mousemove", mousemove); + pv.listen(window, "mouseup", mouseup); + return mousedown; +}; +/** + * Returns a new resize behavior to be registered on mousedown events. + * + * @class Implements interactive resizing of a selection starting with mousedown + * events. Register this behavior on selection handles that should be resizeable + * by the user, such for brushing and linking. This behavior can be used in + * tandom with {@link pv.Behavior.select} and {@link pv.Behavior.drag} to allow + * the selected region to be selected and dragged interactively. + * + * <p>After the initial mousedown event is triggered, this behavior listens for + * mousemove and mouseup events on the window. This allows resizing to continue + * even if the mouse temporarily leaves the assigned panel, or even the root + * panel. + * + * <p>This behavior requires that the data associated with the mark being + * resized have <tt>x</tt>, <tt>y</tt>, <tt>dx</tt> and <tt>dy</tt> attributes + * that correspond to the mark's location and dimensions in pixels. The mark's + * positional properties are not set directly by this behavior; instead, the + * positional properties should be defined as: + * + * <pre> .left(function(d) d.x) + * .top(function(d) d.y) + * .width(function(d) d.dx) + * .height(function(d) d.dy)</pre> + * + * Thus, the behavior does not resize the mark directly, but instead updates the + * size by updating the assigned panel's underlying data. Note that if the + * positional properties are defined with bottom and right (rather than top and + * left), the resize behavior will be inverted, which will confuse users! + * + * <p>The resize behavior is bounded by the assigned mark's enclosing panel; the + * positional attributes are clamped such that the selection does not extend + * outside the panel's bounds. + * + * <p>The mark being resized is automatically re-rendered for each mouse event + * as part of the resize operation. This behavior may be enhanced in the future + * to allow more flexible configuration. In some cases, such as with parallel + * coordinates, resizing the selection may cause related marks to change, in + * which case additional marks may also need to be rendered. This can be + * accomplished by listening for the select psuedo-events:<ul> + * + * <li>resizestart (on mousedown) + * <li>resize (on mousemove) + * <li>resizeend (on mouseup) + * + * </ul>For example, to render the parent panel while resizing, thus + * re-rendering all sibling marks: + * + * <pre> .event("mousedown", pv.Behavior.resize("left")) + * .event("resize", function() this.parent)</pre> + * + * This behavior may be enhanced in the future to allow more flexible + * configuration of the selection behavior. + * + * @extends pv.Behavior + * @see pv.Behavior.select + * @see pv.Behavior.drag + */ +pv.Behavior.resize = function(side) { + var scene, // scene context + index, // scene context + r, // region being selected + m1; // initial mouse position + + /** @private */ + function mousedown(d) { + index = this.index; + scene = this.scene; + m1 = this.mouse(); + r = d; + switch (side) { + case "left": m1.x = r.x + r.dx; break; + case "right": m1.x = r.x; break; + case "top": m1.y = r.y + r.dy; break; + case "bottom": m1.y = r.y; break; + } + pv.Mark.dispatch("resizestart", scene, index); + } + + /** @private */ + function mousemove() { + if (!scene) return; + scene.mark.context(scene, index, function() { + var m2 = this.mouse(); + r.x = Math.max(0, Math.min(m1.x, m2.x)); + r.y = Math.max(0, Math.min(m1.y, m2.y)); + r.dx = Math.min(this.parent.width(), Math.max(m2.x, m1.x)) - r.x; + r.dy = Math.min(this.parent.height(), Math.max(m2.y, m1.y)) - r.y; + this.render(); + }); + pv.Mark.dispatch("resize", scene, index); + } + + /** @private */ + function mouseup() { + if (!scene) return; + pv.Mark.dispatch("resizeend", scene, index); + scene = null; + } + + pv.listen(window, "mousemove", mousemove); + pv.listen(window, "mouseup", mouseup); + return mousedown; +}; +/** + * Returns a new pan behavior to be registered on mousedown events. + * + * @class Implements interactive panning starting with mousedown events. + * Register this behavior on panels to allow panning. This behavior can be used + * in tandem with {@link pv.Behavior.zoom} to allow both panning and zooming: + * + * <pre> .event("mousedown", pv.Behavior.pan()) + * .event("mousewheel", pv.Behavior.zoom())</pre> + * + * The pan behavior currently supports only mouse events; support for keyboard + * shortcuts to improve accessibility may be added in the future. + * + * <p>After the initial mousedown event is triggered, this behavior listens for + * mousemove and mouseup events on the window. This allows panning to continue + * even if the mouse temporarily leaves the panel that is being panned, or even + * the root panel. + * + * <p>The implementation of this behavior relies on the panel's + * <tt>transform</tt> property, which specifies a matrix transformation that is + * applied to child marks. Note that the transform property only affects the + * panel's children, but not the panel itself; therefore the panel's fill and + * stroke will not change when the contents are panned. + * + * <p>Panels have transparent fill styles by default; this means that panels may + * not receive the initial mousedown event to start panning. To fix this + * problem, either given the panel a visible fill style (such as "white"), or + * set the <tt>events</tt> property to "all" such that the panel receives events + * despite its transparent fill. + * + * <p>The pan behavior has optional support for bounding. If enabled, the user + * will not be able to pan the panel outside of the initial bounds. This feature + * is designed to work in conjunction with the zoom behavior; otherwise, + * bounding the panel effectively disables all panning. + * + * @extends pv.Behavior + * @see pv.Behavior.zoom + * @see pv.Panel#transform + */ +pv.Behavior.pan = function() { + var scene, // scene context + index, // scene context + m1, // transformation matrix at the start of panning + v1, // mouse location at the start of panning + k, // inverse scale + bound; // whether to bound to the panel + + /** @private */ + function mousedown() { + index = this.index; + scene = this.scene; + v1 = pv.vector(pv.event.pageX, pv.event.pageY); + m1 = this.transform(); + k = 1 / (m1.k * this.scale); + if (bound) { + bound = { + x: (1 - m1.k) * this.width(), + y: (1 - m1.k) * this.height() + }; + } + } + + /** @private */ + function mousemove() { + if (!scene) return; + scene.mark.context(scene, index, function() { + var x = (pv.event.pageX - v1.x) * k, + y = (pv.event.pageY - v1.y) * k, + m = m1.translate(x, y); + if (bound) { + m.x = Math.max(bound.x, Math.min(0, m.x)); + m.y = Math.max(bound.y, Math.min(0, m.y)); + } + this.transform(m).render(); + }); + pv.Mark.dispatch("pan", scene, index); + } + + /** @private */ + function mouseup() { + scene = null; + } + + /** + * Sets or gets the bound parameter. If bounding is enabled, the user will not + * be able to pan outside the initial panel bounds; this typically applies + * only when the pan behavior is used in tandem with the zoom behavior. + * Bounding is not enabled by default. + * + * <p>Note: enabling bounding after panning has already occurred will not + * immediately reset the transform. Bounding should be enabled before the + * panning behavior is applied. + * + * @function + * @returns {pv.Behavior.pan} this, or the current bound parameter. + * @name pv.Behavior.pan.prototype.bound + * @param {boolean} [x] the new bound parameter. + */ + mousedown.bound = function(x) { + if (arguments.length) { + bound = Boolean(x); + return this; + } + return Boolean(bound); + }; + + pv.listen(window, "mousemove", mousemove); + pv.listen(window, "mouseup", mouseup); + return mousedown; +}; +/** + * Returns a new zoom behavior to be registered on mousewheel events. + * + * @class Implements interactive zooming using mousewheel events. Register this + * behavior on panels to allow zooming. This behavior can be used in tandem with + * {@link pv.Behavior.pan} to allow both panning and zooming: + * + * <pre> .event("mousedown", pv.Behavior.pan()) + * .event("mousewheel", pv.Behavior.zoom())</pre> + * + * The zoom behavior currently supports only mousewheel events; support for + * keyboard shortcuts and gesture events to improve accessibility may be added + * in the future. + * + * <p>The implementation of this behavior relies on the panel's + * <tt>transform</tt> property, which specifies a matrix transformation that is + * applied to child marks. Note that the transform property only affects the + * panel's children, but not the panel itself; therefore the panel's fill and + * stroke will not change when the contents are zoomed. The built-in support for + * transforms only supports uniform scaling and translates, which is sufficient + * for panning and zooming. Note that this is not a strict geometric + * transformation, as the <tt>lineWidth</tt> property is scale-aware: strokes + * are drawn at constant size independent of scale. + * + * <p>Panels have transparent fill styles by default; this means that panels may + * not receive mousewheel events to zoom. To fix this problem, either given the + * panel a visible fill style (such as "white"), or set the <tt>events</tt> + * property to "all" such that the panel receives events despite its transparent + * fill. + * + * <p>The zoom behavior has optional support for bounding. If enabled, the user + * will not be able to zoom out farther than the initial bounds. This feature is + * designed to work in conjunction with the pan behavior. + * + * @extends pv.Behavior + * @see pv.Panel#transform + * @see pv.Mark#scale + * @param {number} speed + */ +pv.Behavior.zoom = function(speed) { + var bound; // whether to bound to the panel + + if (!arguments.length) speed = 1 / 48; + + /** @private */ + function mousewheel() { + var v = this.mouse(), + k = pv.event.wheel * speed, + m = this.transform().translate(v.x, v.y) + .scale((k < 0) ? (1e3 / (1e3 - k)) : ((1e3 + k) / 1e3)) + .translate(-v.x, -v.y); + if (bound) { + m.k = Math.max(1, m.k); + m.x = Math.max((1 - m.k) * this.width(), Math.min(0, m.x)); + m.y = Math.max((1 - m.k) * this.height(), Math.min(0, m.y)); + } + this.transform(m).render(); + pv.Mark.dispatch("zoom", this.scene, this.index); + } + + /** + * Sets or gets the bound parameter. If bounding is enabled, the user will not + * be able to zoom out farther than the initial panel bounds. Bounding is not + * enabled by default. If this behavior is used in tandem with the pan + * behavior, both should use the same bound parameter. + * + * <p>Note: enabling bounding after zooming has already occurred will not + * immediately reset the transform. Bounding should be enabled before the zoom + * behavior is applied. + * + * @function + * @returns {pv.Behavior.zoom} this, or the current bound parameter. + * @name pv.Behavior.zoom.prototype.bound + * @param {boolean} [x] the new bound parameter. + */ + mousewheel.bound = function(x) { + if (arguments.length) { + bound = Boolean(x); + return this; + } + return Boolean(bound); + }; + + return mousewheel; +}; +/** + * @ignore + * @namespace + */ +pv.Geo = function() {}; +/** + * Abstract; not implemented. There is no explicit constructor; this class + * merely serves to document the representation used by {@link pv.Geo.scale}. + * + * @class Represents a pair of geographic coordinates. + * + * @name pv.Geo.LatLng + * @see pv.Geo.scale + */ + +/** + * The <i>latitude</i> coordinate in degrees; positive is North. + * + * @type number + * @name pv.Geo.LatLng.prototype.lat + */ + +/** + * The <i>longitude</i> coordinate in degrees; positive is East. + * + * @type number + * @name pv.Geo.LatLng.prototype.lng + */ +/** + * Abstract; not implemented. There is no explicit constructor; this class + * merely serves to document the representation used by {@link pv.Geo.scale}. + * + * @class Represents a geographic projection. This class provides the core + * implementation for {@link pv.Geo.scale}s, mapping between geographic + * coordinates (latitude and longitude) and normalized screen space in the range + * [-1,1]. The remaining mapping between normalized screen space and actual + * pixels is performed by <tt>pv.Geo.scale</tt>. + * + * <p>Many geographic projections have a point around which the projection is + * centered. Rather than have each implementation add support for a + * user-specified center point, the <tt>pv.Geo.scale</tt> translates the + * geographic coordinates relative to the center point for both the forward and + * inverse projection. + * + * <p>In general, this class should not be used directly, unless the desire is + * to implement a new geographic projection. Instead, use <tt>pv.Geo.scale</tt>. + * Implementations are not required to implement inverse projections, but are + * needed for some forms of interactivity. Also note that some inverse + * projections are ambiguous, such as the connecting points in Dymaxian maps. + * + * @name pv.Geo.Projection + * @see pv.Geo.scale + */ + +/** + * The <i>forward</i> projection. + * + * @function + * @name pv.Geo.Projection.prototype.project + * @param {pv.Geo.LatLng} latlng the latitude and longitude to project. + * @returns {pv.Vector} the xy-coordinates of the given point. + */ + +/** + * The <i>inverse</i> projection; optional. + * + * @function + * @name pv.Geo.Projection.prototype.invert + * @param {pv.Vector} xy the x- and y-coordinates to invert. + * @returns {pv.Geo.LatLng} the latitude and longitude of the given point. + */ +/** + * The built-in projections. + * + * @see pv.Geo.Projection + * @namespace + */ +pv.Geo.projections = { + + /** @see http://en.wikipedia.org/wiki/Mercator_projection */ + mercator: { + project: function(latlng) { + return { + x: latlng.lng / 180, + y: latlng.lat > 85 ? 1 : latlng.lat < -85 ? -1 + : Math.log(Math.tan(Math.PI / 4 + + pv.radians(latlng.lat) / 2)) / Math.PI + }; + }, + invert: function(xy) { + return { + lng: xy.x * 180, + lat: pv.degrees(2 * Math.atan(Math.exp(xy.y * Math.PI)) - Math.PI / 2) + }; + } + }, + + /** @see http://en.wikipedia.org/wiki/Gall-Peters_projection */ + "gall-peters": { + project: function(latlng) { + return { + x: latlng.lng / 180, + y: Math.sin(pv.radians(latlng.lat)) + }; + }, + invert: function(xy) { + return { + lng: xy.x * 180, + lat: pv.degrees(Math.asin(xy.y)) + }; + } + }, + + /** @see http://en.wikipedia.org/wiki/Sinusoidal_projection */ + sinusoidal: { + project: function(latlng) { + return { + x: pv.radians(latlng.lng) * Math.cos(pv.radians(latlng.lat)) / Math.PI, + y: latlng.lat / 90 + }; + }, + invert: function(xy) { + return { + lng: pv.degrees((xy.x * Math.PI) / Math.cos(xy.y * Math.PI / 2)), + lat: xy.y * 90 + }; + } + }, + + /** @see http://en.wikipedia.org/wiki/Aitoff_projection */ + aitoff: { + project: function(latlng) { + var l = pv.radians(latlng.lng), + f = pv.radians(latlng.lat), + a = Math.acos(Math.cos(f) * Math.cos(l / 2)); + return { + x: 2 * (a ? (Math.cos(f) * Math.sin(l / 2) * a / Math.sin(a)) : 0) / Math.PI, + y: 2 * (a ? (Math.sin(f) * a / Math.sin(a)) : 0) / Math.PI + }; + }, + invert: function(xy) { + var x = xy.x * Math.PI / 2, + y = xy.y * Math.PI / 2; + return { + lng: pv.degrees(x / Math.cos(y)), + lat: pv.degrees(y) + }; + } + }, + + /** @see http://en.wikipedia.org/wiki/Hammer_projection */ + hammer: { + project: function(latlng) { + var l = pv.radians(latlng.lng), + f = pv.radians(latlng.lat), + c = Math.sqrt(1 + Math.cos(f) * Math.cos(l / 2)); + return { + x: 2 * Math.SQRT2 * Math.cos(f) * Math.sin(l / 2) / c / 3, + y: Math.SQRT2 * Math.sin(f) / c / 1.5 + }; + }, + invert: function(xy) { + var x = xy.x * 3, + y = xy.y * 1.5, + z = Math.sqrt(1 - x * x / 16 - y * y / 4); + return { + lng: pv.degrees(2 * Math.atan2(z * x, 2 * (2 * z * z - 1))), + lat: pv.degrees(Math.asin(z * y)) + }; + } + }, + + /** The identity or "none" projection. */ + identity: { + project: function(latlng) { + return { + x: latlng.lng / 180, + y: latlng.lat / 90 + }; + }, + invert: function(xy) { + return { + lng: xy.x * 180, + lat: xy.y * 90 + }; + } + } +}; +/** + * Returns a geographic scale. The arguments to this constructor are optional, + * and equivalent to calling {@link #projection}. + * + * @class Represents a geographic scale; a mapping between latitude-longitude + * coordinates and screen pixel coordinates. By default, the domain is inferred + * from the geographic coordinates, so that the domain fills the output range. + * + * <p>Note that geographic scales are two-dimensional transformations, rather + * than the one-dimensional bidrectional mapping typical of other scales. + * Rather than mapping (for example) between a numeric domain and a numeric + * range, geographic scales map between two coordinate objects: {@link + * pv.Geo.LatLng} and {@link pv.Vector}. + * + * @param {pv.Geo.Projection} [p] optional projection. + * @see pv.Geo.scale#ticks + */ +pv.Geo.scale = function(p) { + var rmin = {x: 0, y: 0}, // default range minimum + rmax = {x: 1, y: 1}, // default range maximum + d = [], // default domain + j = pv.Geo.projections.identity, // domain <-> normalized range + x = pv.Scale.linear(-1, 1).range(0, 1), // normalized <-> range + y = pv.Scale.linear(-1, 1).range(1, 0), // normalized <-> range + c = {lng: 0, lat: 0}, // Center Point + lastLatLng, // cached latlng + lastPoint; // cached point + + /** @private */ + function scale(latlng) { + if (!lastLatLng + || (latlng.lng != lastLatLng.lng) + || (latlng.lat != lastLatLng.lat)) { + lastLatLng = latlng; + var p = project(latlng); + lastPoint = {x: x(p.x), y: y(p.y)}; + } + return lastPoint; + } + + /** @private */ + function project(latlng) { + var offset = {lng: latlng.lng - c.lng, lat: latlng.lat}; + return j.project(offset); + } + + /** @private */ + function invert(xy) { + var latlng = j.invert(xy); + latlng.lng += c.lng; + return latlng; + } + + /** Returns the projected x-coordinate. */ + scale.x = function(latlng) { + return scale(latlng).x; + }; + + /** Returns the projected y-coordinate. */ + scale.y = function(latlng) { + return scale(latlng).y; + }; + + /** + * Abstract; this is a local namespace on a given geographic scale. + * + * @namespace Tick functions for geographic scales. Because geographic scales + * represent two-dimensional transformations (as opposed to one-dimensional + * transformations typical of other scales), the tick values are similarly + * represented as two-dimensional coordinates in the input domain, i.e., + * {@link pv.Geo.LatLng} objects. + * + * <p>Also, note that non-rectilinear projections, such as sinsuoidal and + * aitoff, may not produce straight lines for constant longitude or constant + * latitude. Therefore the returned array of ticks is a two-dimensional array, + * sampling various latitudes as constant longitude, and vice versa. + * + * <p>The tick lines can therefore be approximated as polylines, either with + * "linear" or "cardinal" interpolation. This is not as accurate as drawing + * the true curve through the projection space, but is usually sufficient. + * + * @name pv.Geo.scale.prototype.ticks + * @see pv.Geo.scale + * @see pv.Geo.LatLng + * @see pv.Line#interpolate + */ + scale.ticks = { + + /** + * Returns longitude ticks. + * + * @function + * @param {number} [m] the desired number of ticks. + * @returns {array} a nested array of <tt>pv.Geo.LatLng</tt> ticks. + * @name pv.Geo.scale.prototype.ticks.prototype.lng + */ + lng: function(m) { + var lat, lng; + if (d.length > 1) { + var s = pv.Scale.linear(); + if (m == undefined) m = 10; + lat = s.domain(d, function(d) { return d.lat; }).ticks(m); + lng = s.domain(d, function(d) { return d.lng; }).ticks(m); + } else { + lat = pv.range(-80, 81, 10); + lng = pv.range(-180, 181, 10); + } + return lng.map(function(lng) { + return lat.map(function(lat) { + return {lat: lat, lng: lng}; + }); + }); + }, + + /** + * Returns latitude ticks. + * + * @function + * @param {number} [m] the desired number of ticks. + * @returns {array} a nested array of <tt>pv.Geo.LatLng</tt> ticks. + * @name pv.Geo.scale.prototype.ticks.prototype.lat + */ + lat: function(m) { + return pv.transpose(scale.ticks.lng(m)); + } + }; + + /** + * Inverts the specified value in the output range, returning the + * corresponding value in the input domain. This is frequently used to convert + * the mouse location (see {@link pv.Mark#mouse}) to a value in the input + * domain. Inversion is only supported for numeric ranges, and not colors. + * + * <p>Note that this method does not do any rounding or bounds checking. If + * the input domain is discrete (e.g., an array index), the returned value + * should be rounded. If the specified <tt>y</tt> value is outside the range, + * the returned value may be equivalently outside the input domain. + * + * @function + * @name pv.Geo.scale.prototype.invert + * @param {number} y a value in the output range (a pixel location). + * @returns {number} a value in the input domain. + */ + scale.invert = function(p) { + return invert({x: x.invert(p.x), y: y.invert(p.y)}); + }; + + /** + * Sets or gets the input domain. Note that unlike quantitative scales, the + * domain cannot be reduced to a simple rectangle (i.e., minimum and maximum + * values for latitude and longitude). Instead, the domain values must be + * projected to normalized space, effectively finding the domain in normalized + * space rather than in terms of latitude and longitude. Thus, changing the + * projection requires recomputing the normalized domain. + * + * <p>This method can be invoked several ways: + * + * <p>1. <tt>domain(values...)</tt> + * + * <p>Specifying the domain as a series of {@link pv.Geo.LatLng}s is the most + * explicit and recommended approach. However, if the domain values are + * derived from data, you may find the second method more appropriate. + * + * <p>2. <tt>domain(array, f)</tt> + * + * <p>Rather than enumerating the domain explicitly, you can specify a single + * argument of an array. In addition, you can specify an optional accessor + * function to extract the domain values (as {@link pv.Geo.LatLng}s) from the + * array. If the specified array has fewer than two elements, this scale will + * default to the full normalized domain. + * + * <p>2. <tt>domain()</tt> + * + * <p>Invoking the <tt>domain</tt> method with no arguments returns the + * current domain as an array. + * + * @function + * @name pv.Geo.scale.prototype.domain + * @param {...} domain... domain values. + * @returns {pv.Geo.scale} <tt>this</tt>, or the current domain. + */ + scale.domain = function(array, f) { + if (arguments.length) { + d = (array instanceof Array) + ? ((arguments.length > 1) ? pv.map(array, f) : array) + : Array.prototype.slice.call(arguments); + if (d.length > 1) { + var lngs = d.map(function(c) { return c.lng; }); + var lats = d.map(function(c) { return c.lat; }); + c = { + lng: (pv.max(lngs) + pv.min(lngs)) / 2, + lat: (pv.max(lats) + pv.min(lats)) / 2 + }; + var n = d.map(project); // normalized domain + x.domain(n, function(p) { return p.x; }); + y.domain(n, function(p) { return p.y; }); + } else { + c = {lng: 0, lat: 0}; + x.domain(-1, 1); + y.domain(-1, 1); + } + lastLatLng = null; // invalidate the cache + return this; + } + return d; + }; + + /** + * Sets or gets the output range. This method can be invoked several ways: + * + * <p>1. <tt>range(min, max)</tt> + * + * <p>If two objects are specified, the arguments should be {@link pv.Vector}s + * which specify the minimum and maximum values of the x- and y-coordinates + * explicitly. + * + * <p>2. <tt>range(width, height)</tt> + * + * <p>If two numbers are specified, the arguments specify the maximum values + * of the x- and y-coordinates explicitly; the minimum values are implicitly + * zero. + * + * <p>3. <tt>range()</tt> + * + * <p>Invoking the <tt>range</tt> method with no arguments returns the current + * range as an array of two {@link pv.Vector}s: the minimum (top-left) and + * maximum (bottom-right) values. + * + * @function + * @name pv.Geo.scale.prototype.range + * @param {...} range... range values. + * @returns {pv.Geo.scale} <tt>this</tt>, or the current range. + */ + scale.range = function(min, max) { + if (arguments.length) { + if (typeof min == "object") { + rmin = {x: Number(min.x), y: Number(min.y)}; + rmax = {x: Number(max.x), y: Number(max.y)}; + } else { + rmin = {x: 0, y: 0}; + rmax = {x: Number(min), y: Number(max)}; + } + x.range(rmin.x, rmax.x); + y.range(rmax.y, rmin.y); // XXX flipped? + lastLatLng = null; // invalidate the cache + return this; + } + return [rmin, rmax]; + }; + + /** + * Sets or gets the projection. This method can be invoked several ways: + * + * <p>1. <tt>projection(string)</tt> + * + * <p>Specifying a string sets the projection to the given named projection in + * {@link pv.Geo.projections}. If no such projection is found, the identity + * projection is used. + * + * <p>2. <tt>projection(object)</tt> + * + * <p>Specifying an object sets the projection to the given custom projection, + * which must implement the <i>forward</i> and <i>inverse</i> methods per the + * {@link pv.Geo.Projection} interface. + * + * <p>3. <tt>projection()</tt> + * + * <p>Invoking the <tt>projection</tt> method with no arguments returns the + * current object that defined the projection. + * + * @function + * @name pv.Scale.geo.prototype.projection + * @param {...} range... range values. + * @returns {pv.Scale.geo} <tt>this</tt>, or the current range. + */ + scale.projection = function(p) { + if (arguments.length) { + j = typeof p == "string" + ? pv.Geo.projections[p] || pv.Geo.projections.identity + : p; + return this.domain(d); // recompute normalized domain + } + return p; + }; + + /** + * Returns a view of this scale by the specified accessor function <tt>f</tt>. + * Given a scale <tt>g</tt>, <tt>g.by(function(d) d.foo)</tt> is equivalent to + * <tt>function(d) g(d.foo)</tt>. This method should be used judiciously; it + * is typically more clear to invoke the scale directly, passing in the value + * to be scaled. + * + * @function + * @name pv.Geo.scale.prototype.by + * @param {function} f an accessor function. + * @returns {pv.Geo.scale} a view of this scale by the specified accessor + * function. + */ + scale.by = function(f) { + function by() { return scale(f.apply(this, arguments)); } + for (var method in scale) by[method] = scale[method]; + return by; + }; + + if (arguments.length) scale.projection(p); + return scale; +}; diff --git a/sonar-server/src/main/webapp/javascripts/scriptaculous.js b/sonar-server/src/main/webapp/javascripts/scriptaculous.js index e291e44f222..23fcfe8fbe2 100644 --- a/sonar-server/src/main/webapp/javascripts/scriptaculous.js +++ b/sonar-server/src/main/webapp/javascripts/scriptaculous.js @@ -1,6 +1,6 @@ -// script.aculo.us builder.js v1.8.3, Thu Oct 08 11:23:33 +0200 2009 +// script.aculo.us builder.js v1.9.0, Thu Dec 23 16:54:48 -0500 2010 -// Copyright (c) 2005-2009 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) +// Copyright (c) 2005-2010 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) // // script.aculo.us is freely distributable under the terms of an MIT-style license. // For details, see the script.aculo.us web site: http://script.aculo.us/ @@ -134,11 +134,9 @@ var Builder = { }); } }; +// script.aculo.us effects.js v1.9.0, Thu Dec 23 16:54:48 -0500 2010 - -// script.aculo.us effects.js v1.8.3, Thu Oct 08 11:23:33 +0200 2009 - -// Copyright (c) 2005-2009 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) +// Copyright (c) 2005-2010 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) // Contributors: // Justin Palmer (http://encytemedia.com/) // Mark Pilgrim (http://diveintomark.org/) @@ -1260,12 +1258,11 @@ $w('getInlineOpacity forceRerendering setContentZoom collectTextNodes collectTex Element.addMethods(Effect.Methods); +// script.aculo.us controls.js v1.9.0, Thu Dec 23 16:54:48 -0500 2010 -// script.aculo.us controls.js v1.8.3, Thu Oct 08 11:23:33 +0200 2009 - -// Copyright (c) 2005-2009 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) -// (c) 2005-2009 Ivan Krstic (http://blogs.law.harvard.edu/ivan) -// (c) 2005-2009 Jon Tirsen (http://www.tirsen.com) +// Copyright (c) 2005-2010 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) +// (c) 2005-2010 Ivan Krstic (http://blogs.law.harvard.edu/ivan) +// (c) 2005-2010 Jon Tirsen (http://www.tirsen.com) // Contributors: // Richard Livsey // Rahul Bhargava @@ -1506,10 +1503,10 @@ Autocompleter.Base = Class.create({ var value = ''; if (this.options.select) { var nodes = $(selectedElement).select('.' + this.options.select) || []; - if(nodes.length>0) value = Element.collectTextNodes(nodes[0], this.options.select); + if(nodes.length>0) value = Element.collectTextNodes(nodes[0], this.options.select); } else value = Element.collectTextNodesIgnoreClass(selectedElement, 'informal'); - + var bounds = this.getTokenBounds(); if (bounds[0] != -1) { var newValue = this.element.value.substr(0, bounds[0]); @@ -1521,9 +1518,12 @@ Autocompleter.Base = Class.create({ this.element.value = value; } this.oldElementValue = this.element.value; - // Following line was commented for SONAR-1688 because in our autosuggest text fields, we use + + // SONAR + // Following line was commented for SONAR-1688 because in our autosuggest text fields, we use // the onfocus() method to reinitialize the value of the input field to ''. //this.element.focus(); + // /SONAR if (this.options.afterUpdateElement) this.options.afterUpdateElement(this.element, selectedElement); @@ -2228,11 +2228,9 @@ Form.Element.DelayedObserver = Class.create({ this.callback(this.element, $F(this.element)); } }); +// script.aculo.us dragdrop.js v1.9.0, Thu Dec 23 16:54:48 -0500 2010 - -// script.aculo.us dragdrop.js v1.8.3, Thu Oct 08 11:23:33 +0200 2009 - -// Copyright (c) 2005-2009 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) +// Copyright (c) 2005-2010 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) // // script.aculo.us is freely distributable under the terms of an MIT-style license. // For details, see the script.aculo.us web site: http://script.aculo.us/ @@ -2606,7 +2604,7 @@ var Draggable = Class.create({ if (this.options.scroll == window) { with(this._getWindowScroll(this.options.scroll)) { p = [ left, top, left+width, top+height ]; } } else { - p = Position.page(this.options.scroll); + p = Position.page(this.options.scroll).toArray(); p[0] += this.options.scroll.scrollLeft + Position.deltaX; p[1] += this.options.scroll.scrollTop + Position.deltaY; p.push(p[0]+this.options.scroll.offsetWidth); diff --git a/sonar-server/src/main/webapp/stylesheets/style.css b/sonar-server/src/main/webapp/stylesheets/style.css index 39f88114558..6ebe3749880 100644 --- a/sonar-server/src/main/webapp/stylesheets/style.css +++ b/sonar-server/src/main/webapp/stylesheets/style.css @@ -1792,6 +1792,17 @@ table.matrix tbody td.title { padding: 5px 0 0 5px; } +div.widget-matrix { + overflow:auto; + font-size: 12px; + padding: 1px; +} + +div.widget-matrix th { + text-align: right; + font-weight: normal; +} + a.nolink, .dashbox a, .dashbox a:visited { text-decoration: none; diff --git a/sonar-server/src/test/java/org/sonar/server/database/JndiDatabaseConnectorTest.java b/sonar-server/src/test/java/org/sonar/server/database/JndiDatabaseConnectorTest.java index 37e28fb7b5d..e3874c385af 100644 --- a/sonar-server/src/test/java/org/sonar/server/database/JndiDatabaseConnectorTest.java +++ b/sonar-server/src/test/java/org/sonar/server/database/JndiDatabaseConnectorTest.java @@ -28,31 +28,25 @@ import org.sonar.api.database.DatabaseProperties; import org.sonar.jpa.entity.SchemaMigration; import javax.naming.Context; -import javax.naming.spi.InitialContextFactory; import javax.persistence.EntityManagerFactory; -import javax.sql.DataSource; -import java.sql.Connection; -import java.sql.DatabaseMetaData; -import java.util.Hashtable; import static org.junit.Assert.assertEquals; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.mock; public class JndiDatabaseConnectorTest { - private Configuration conf; private String currentInitialContextFacto; private JndiDatabaseConnector connector; private int emfCreationCounter; @Before public void setup() { - conf = new PropertiesConfiguration(); - conf.setProperty(DatabaseProperties.PROP_JNDI_NAME, "jdbc/sonar"); + Configuration conf = new PropertiesConfiguration(); conf.setProperty(DatabaseProperties.PROP_DIALECT, DatabaseProperties.DIALECT_HSQLDB); + conf.setProperty(DatabaseProperties.PROP_URL, "jdbc:hsqldb:mem:sonar"); + conf.setProperty(DatabaseProperties.PROP_DRIVER, "org.hsqldb.jdbcDriver"); currentInitialContextFacto = System.getProperty(Context.INITIAL_CONTEXT_FACTORY); connector = getTestJndiConnector(conf); - System.setProperty(Context.INITIAL_CONTEXT_FACTORY, TestInitialContextFactory.class.getName()); } @After @@ -75,16 +69,6 @@ public class JndiDatabaseConnectorTest { assertEquals(2, emfCreationCounter); } - @Test - public void transactionIsolationCorrectlySet() throws Exception { - int fakeTransactionIsolation = 9; - conf.setProperty(DatabaseProperties.PROP_ISOLATION, fakeTransactionIsolation); - connector.start(); - Connection c = connector.getConnection(); - // start method call get a connection to test it, so total number is 2 - verify(c, times(2)).setTransactionIsolation(fakeTransactionIsolation); - } - private JndiDatabaseConnector getTestJndiConnector(Configuration conf) { JndiDatabaseConnector connector = new JndiDatabaseConnector(conf) { @Override @@ -102,28 +86,5 @@ public class JndiDatabaseConnectorTest { return connector; } - public static class TestInitialContextFactory implements InitialContextFactory { - - private Connection c; - - public Context getInitialContext(Hashtable env) { - Context envContext = mock(Context.class); - Context context = mock(Context.class); - DataSource ds = mock(DataSource.class); - DatabaseMetaData m = mock(DatabaseMetaData.class); - c = mock(Connection.class); - try { - when(envContext.lookup(JndiDatabaseConnector.JNDI_ENV_CONTEXT)).thenReturn(context); - when(context.lookup("jdbc/sonar")).thenReturn(ds); - when(ds.getConnection()).thenReturn(c); - when(m.getURL()).thenReturn("jdbc:test"); - when(c.getMetaData()).thenReturn(m); - } catch (Exception e) { - throw new RuntimeException(e); - } - - return envContext; - } - } } diff --git a/sonar-server/src/test/java/org/sonar/server/platform/ServerIdGeneratorTest.java b/sonar-server/src/test/java/org/sonar/server/platform/ServerIdGeneratorTest.java new file mode 100644 index 00000000000..354e19435ea --- /dev/null +++ b/sonar-server/src/test/java/org/sonar/server/platform/ServerIdGeneratorTest.java @@ -0,0 +1,82 @@ +/* + * Sonar, open source software quality management tool. + * Copyright (C) 2008-2011 SonarSource + * mailto:contact AT sonarsource DOT com + * + * Sonar is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * Sonar is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with Sonar; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package org.sonar.server.platform; + +import org.apache.commons.lang.StringUtils; +import org.hamcrest.core.Is; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.net.InetAddress; +import java.net.UnknownHostException; + +import static org.hamcrest.text.StringStartsWith.startsWith; +import static org.junit.Assert.assertThat; + +public class ServerIdGeneratorTest { + + private static InetAddress localhost; + + @BeforeClass + public static void init() throws UnknownHostException { + localhost = InetAddress.getLocalHost(); + } + + @Test + public void idShouldHaveTenCharacters() { + String id = new ServerIdGenerator().toId("SonarSource", localhost); + assertThat(id.length(), Is.is(15)); // first character is version + 14 characters for checksum + assertThat(StringUtils.isBlank(id), Is.is(false)); + } + + @Test + public void idShouldStartWithVersion() { + String id = new ServerIdGenerator().toId("SonarSource", localhost); + assertThat(id, startsWith(ServerIdGenerator.VERSION)); + } + + @Test + public void loopbackAddressesShouldNotBeAccepted() throws UnknownHostException { + assertThat(new ServerIdGenerator().isFixed(InetAddress.getByName("127.0.0.1")), Is.is(false)); + } + + @Test + public void publicAddressesNotBeAccepted() throws UnknownHostException { + assertThat(new ServerIdGenerator().isFixed(InetAddress.getByName("sonarsource.com")), Is.is(true)); + } + + @Test + public void idShouldBeUniquePerOrganisation() { + ServerIdGenerator generator = new ServerIdGenerator(true); + + String k1 = generator.generate("Corp One", "127.0.0.1"); + String k2 = generator.generate("Corp Two", "127.0.0.1"); + assertThat(StringUtils.equals(k1, k2), Is.is(false)); + } + + @Test + public void idShouldBeReproducible() { + ServerIdGenerator generator = new ServerIdGenerator(true); + String i1 = generator.generate("SonarSource", "127.0.0.1"); + String i2 = generator.generate("SonarSource", "127.0.0.1"); + assertThat(StringUtils.equals(i1, i2), Is.is(true)); + } + +} diff --git a/sonar-server/src/test/java/org/sonar/server/platform/ServerImplTest.java b/sonar-server/src/test/java/org/sonar/server/platform/ServerImplTest.java index 583772a50bb..145970608c5 100644 --- a/sonar-server/src/test/java/org/sonar/server/platform/ServerImplTest.java +++ b/sonar-server/src/test/java/org/sonar/server/platform/ServerImplTest.java @@ -19,18 +19,21 @@ */ package org.sonar.server.platform; +import org.hamcrest.core.Is; import org.junit.Test; +import org.sonar.jpa.test.AbstractDbUnitTestCase; import java.io.IOException; +import java.util.Date; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.*; -public class ServerImplTest { +public class ServerImplTest extends AbstractDbUnitTestCase { @Test public void alwaysReturnTheSameValues() { - ServerImpl server = new ServerImpl(); + ServerImpl server = new ServerImpl(getSessionFactory()); + server.start(); assertNotNull(server.getId()); assertEquals(server.getId(), server.getId()); @@ -44,21 +47,31 @@ public class ServerImplTest { @Test public void getVersionFromFile() throws IOException { - assertEquals("1.0", new ServerImpl().loadVersionFromManifest("/org/sonar/server/platform/ServerImplTest/pom-with-version.properties")); + assertEquals("1.0", new ServerImpl(getSessionFactory()).loadVersionFromManifest("/org/sonar/server/platform/ServerImplTest/pom-with-version.properties")); } @Test public void testFileWithNoVersion() throws IOException { - assertEquals("", new ServerImpl().loadVersionFromManifest("/org/sonar/server/platform/ServerImplTest/pom-without-version.properties")); + assertEquals("", new ServerImpl(getSessionFactory()).loadVersionFromManifest("/org/sonar/server/platform/ServerImplTest/pom-without-version.properties")); } @Test public void testFileWithEmptyVersionParameter() throws IOException { - assertEquals("", new ServerImpl().loadVersionFromManifest("/org/sonar/server/platform/ServerImplTest/pom-with-empty-version.properties")); + assertEquals("", new ServerImpl(getSessionFactory()).loadVersionFromManifest("/org/sonar/server/platform/ServerImplTest/pom-with-empty-version.properties")); } @Test public void shouldNotFailIfFileNotFound() throws IOException { - assertEquals("", new ServerImpl().loadVersionFromManifest("/org/sonar/server/platform/ServerImplTest/unknown-file.properties")); + assertEquals("", new ServerImpl(getSessionFactory()).loadVersionFromManifest("/org/sonar/server/platform/ServerImplTest/unknown-file.properties")); + } + + @Test + public void shouldLoadServerIdFromDatabase() { + setupData("shouldLoadServerIdFromDatabase"); + + ServerImpl server = new ServerImpl(getSessionFactory(), new Date()); + server.start(); + + assertThat(server.getPermanentServerId(), Is.is("abcde")); } } diff --git a/sonar-server/src/test/java/org/sonar/server/platform/ServerLifecycleNotifierTest.java b/sonar-server/src/test/java/org/sonar/server/platform/ServerLifecycleNotifierTest.java index 0e980f3983d..efa3a4adae1 100644 --- a/sonar-server/src/test/java/org/sonar/server/platform/ServerLifecycleNotifierTest.java +++ b/sonar-server/src/test/java/org/sonar/server/platform/ServerLifecycleNotifierTest.java @@ -99,4 +99,8 @@ class FakeServer extends Server { public String getURL() { return null; } + + public String getPermanentServerId() { + return null; + } } diff --git a/sonar-server/src/test/java/org/sonar/server/plugins/ServerPluginRepositoryTest.java b/sonar-server/src/test/java/org/sonar/server/plugins/DefaultServerPluginRepositoryTest.java index dcc85e5b519..afe742b1887 100644 --- a/sonar-server/src/test/java/org/sonar/server/plugins/ServerPluginRepositoryTest.java +++ b/sonar-server/src/test/java/org/sonar/server/plugins/DefaultServerPluginRepositoryTest.java @@ -26,22 +26,25 @@ import org.junit.Test; import org.picocontainer.containers.TransientPicoContainer; import org.sonar.api.*; import org.sonar.api.platform.PluginMetadata; +import org.sonar.batch.Batch; import org.sonar.core.plugins.DefaultPluginMetadata; import java.io.File; import java.util.Arrays; import java.util.List; +import static junit.framework.Assert.assertFalse; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.nullValue; import static org.hamcrest.core.IsNot.not; import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -public class ServerPluginRepositoryTest { +public class DefaultServerPluginRepositoryTest { - private ServerPluginRepository repository; + private DefaultServerPluginRepository repository; @After public void stop() { @@ -53,14 +56,14 @@ public class ServerPluginRepositoryTest { @Test public void testStart() { PluginDeployer deployer = mock(PluginDeployer.class); - File pluginFile = FileUtils.toFile(getClass().getResource("/org/sonar/server/plugins/ServerPluginRepositoryTest/sonar-artifact-size-plugin-0.2.jar")); + File pluginFile = FileUtils.toFile(getClass().getResource("/org/sonar/server/plugins/DefaultServerPluginRepositoryTest/sonar-artifact-size-plugin-0.2.jar")); PluginMetadata plugin = DefaultPluginMetadata.create(pluginFile) .setKey("artifactsize") .setMainClass("org.sonar.plugins.artifactsize.ArtifactSizePlugin") .addDeployedFile(pluginFile); when(deployer.getMetadata()).thenReturn(Arrays.asList(plugin)); - repository = new ServerPluginRepository(deployer); + repository = new DefaultServerPluginRepository(deployer); repository.start(); assertThat(repository.getPlugins().size(), Is.is(1)); @@ -73,7 +76,7 @@ public class ServerPluginRepositoryTest { @Test public void shouldRegisterServerExtensions() { - ServerPluginRepository repository = new ServerPluginRepository(mock(PluginDeployer.class)); + DefaultServerPluginRepository repository = new DefaultServerPluginRepository(mock(PluginDeployer.class)); TransientPicoContainer container = new TransientPicoContainer(); repository.registerExtensions(container, Arrays.<Plugin>asList(new FakePlugin(Arrays.<Class>asList(FakeBatchExtension.class, FakeServerExtension.class)))); @@ -84,8 +87,8 @@ public class ServerPluginRepositoryTest { } @Test - public void shouldInvokeServerExtensionProviderss() { - ServerPluginRepository repository = new ServerPluginRepository(mock(PluginDeployer.class)); + public void shouldInvokeServerExtensionProviders() { + DefaultServerPluginRepository repository = new DefaultServerPluginRepository(mock(PluginDeployer.class)); TransientPicoContainer container = new TransientPicoContainer(); repository.registerExtensions(container, Arrays.<Plugin>asList(new FakePlugin(Arrays.<Class>asList(FakeExtensionProvider.class)))); @@ -95,6 +98,68 @@ public class ServerPluginRepositoryTest { assertThat(container.getComponents(FakeBatchExtension.class).size(), is(0)); } + @Test + public void shouldNotSupportProvidersOfProviders() { + DefaultServerPluginRepository repository = new DefaultServerPluginRepository(mock(PluginDeployer.class)); + + TransientPicoContainer container = new TransientPicoContainer(); + repository.registerExtensions(container, Arrays.<Plugin>asList(new FakePlugin(Arrays.<Class>asList(SuperExtensionProvider.class)))); + + assertThat(container.getComponents(FakeBatchExtension.class).size(), is(0)); + assertThat(container.getComponents(FakeServerExtension.class).size(), is(0)); + } + + @Test + public void shouldDisablePlugin() { + DefaultServerPluginRepository repository = new DefaultServerPluginRepository(mock(PluginDeployer.class)); + repository.disable("checkstyle"); + + assertTrue(repository.isDisabled("checkstyle")); + assertFalse(repository.isDisabled("sqale")); + } + + @Test + public void shouldDisableDependentPlugins() { + PluginDeployer deployer = mock(PluginDeployer.class); + List<PluginMetadata> metadata = Arrays.asList( + newMetadata("checkstyle", null), + newMetadata("checkstyle-extensions", "checkstyle"), + newMetadata("sqale", null) + ); + when(deployer.getMetadata()).thenReturn(metadata); + DefaultServerPluginRepository repository = new DefaultServerPluginRepository(deployer); + + repository.disable("checkstyle"); + + assertTrue(repository.isDisabled("checkstyle")); + assertTrue(repository.isDisabled("checkstyle-extensions")); + assertFalse(repository.isDisabled("sqale")); + } + + @Test + public void shouldNotDisableBasePlugin() { + PluginDeployer deployer = mock(PluginDeployer.class); + List<PluginMetadata> metadata = Arrays.asList( + newMetadata("checkstyle", null), + newMetadata("checkstyle-extensions", "checkstyle"), + newMetadata("sqale", null) + ); + when(deployer.getMetadata()).thenReturn(metadata); + DefaultServerPluginRepository repository = new DefaultServerPluginRepository(deployer); + + repository.disable("checkstyle-extensions"); + + assertFalse(repository.isDisabled("checkstyle")); + assertTrue(repository.isDisabled("checkstyle-extensions")); + } + + private PluginMetadata newMetadata(String pluginKey, String basePluginKey) { + PluginMetadata plugin = mock(PluginMetadata.class); + when(plugin.getKey()).thenReturn(pluginKey); + when(plugin.getBasePlugin()).thenReturn(basePluginKey); + return plugin; + } + public static class FakePlugin extends SonarPlugin { private List<Class> extensions; @@ -122,4 +187,12 @@ public class ServerPluginRepositoryTest { return Arrays.asList(FakeBatchExtension.class, FakeServerExtension.class); } } + + public static class SuperExtensionProvider extends ExtensionProvider implements ServerExtension { + + @Override + public Object provide() { + return FakeExtensionProvider.class; + } + } } diff --git a/sonar-server/src/test/java/org/sonar/server/startup/GeneratePluginIndexTest.java b/sonar-server/src/test/java/org/sonar/server/startup/GeneratePluginIndexTest.java new file mode 100644 index 00000000000..38c26893317 --- /dev/null +++ b/sonar-server/src/test/java/org/sonar/server/startup/GeneratePluginIndexTest.java @@ -0,0 +1,89 @@ +/* + * Sonar, open source software quality management tool. + * Copyright (C) 2008-2011 SonarSource + * mailto:contact AT sonarsource DOT com + * + * Sonar is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * Sonar is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with Sonar; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package org.sonar.server.startup; + +import org.apache.commons.io.FileUtils; +import org.hamcrest.core.Is; +import org.junit.Before; +import org.junit.Test; +import org.sonar.api.platform.PluginMetadata; +import org.sonar.core.plugins.DefaultPluginMetadata; +import org.sonar.server.platform.DefaultServerFileSystem; +import org.sonar.server.plugins.DefaultServerPluginRepository; + +import java.io.File; +import java.io.IOException; +import java.util.Arrays; +import java.util.List; + +import static org.junit.Assert.assertThat; +import static org.junit.matchers.JUnitMatchers.containsString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class GeneratePluginIndexTest { + + private DefaultServerFileSystem fileSystem; + private File index; + + @Before + public void createIndexFile() { + fileSystem = mock(DefaultServerFileSystem.class); + index = new File("target/test-tmp/GeneratePluginIndexTest/plugins.txt"); + when(fileSystem.getPluginIndex()).thenReturn(index); + } + + @Test + public void shouldWriteIndex() throws IOException { + DefaultServerPluginRepository repository = mock(DefaultServerPluginRepository.class); + PluginMetadata sqale = newMetadata("sqale"); + PluginMetadata checkstyle = newMetadata("checkstyle"); + when(repository.getMetadata()).thenReturn(Arrays.asList(sqale, checkstyle)); + + new GeneratePluginIndex(fileSystem, repository).start(); + + List<String> lines = FileUtils.readLines(index); + assertThat(lines.size(), Is.is(2)); + assertThat(lines.get(0), containsString("sqale")); + assertThat(lines.get(1), containsString("checkstyle")); + } + + @Test + public void shouldSkipDisabledPlugin() throws IOException { + DefaultServerPluginRepository repository = mock(DefaultServerPluginRepository.class); + PluginMetadata sqale = newMetadata("sqale"); + PluginMetadata checkstyle = newMetadata("checkstyle"); + when(repository.getMetadata()).thenReturn(Arrays.asList(sqale, checkstyle)); + when(repository.isDisabled("checkstyle")).thenReturn(true); + + new GeneratePluginIndex(fileSystem, repository).start(); + + List<String> lines = FileUtils.readLines(index); + assertThat(lines.size(), Is.is(1)); + assertThat(lines.get(0), containsString("sqale")); + } + + private PluginMetadata newMetadata(String pluginKey) { + PluginMetadata plugin = mock(DefaultPluginMetadata.class); + when(plugin.getKey()).thenReturn(pluginKey); + when(plugin.getFile()).thenReturn(new File(pluginKey + ".jar")); + return plugin; + } +} diff --git a/sonar-server/src/test/java/org/sonar/server/startup/ServerMetadataPersisterTest.java b/sonar-server/src/test/java/org/sonar/server/startup/ServerMetadataPersisterTest.java index 81ed38316dd..4a22f851b6d 100644 --- a/sonar-server/src/test/java/org/sonar/server/startup/ServerMetadataPersisterTest.java +++ b/sonar-server/src/test/java/org/sonar/server/startup/ServerMetadataPersisterTest.java @@ -19,42 +19,72 @@ */ package org.sonar.server.startup; +import org.junit.After; +import org.junit.Before; import org.junit.Test; -import org.sonar.jpa.test.AbstractDbUnitTestCase; import org.sonar.api.platform.Server; -import org.sonar.server.platform.ServerImpl; +import org.sonar.jpa.test.AbstractDbUnitTestCase; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; import java.util.TimeZone; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + public class ServerMetadataPersisterTest extends AbstractDbUnitTestCase { + private TimeZone initialTimeZone; + + @Before + public void fixTimeZone() { + initialTimeZone = TimeZone.getDefault(); + TimeZone.setDefault(TimeZone.getTimeZone("GMT")); + } + + @After + public void revertTimeZone() { + TimeZone.setDefault(initialTimeZone); + } + @Test public void testSaveProperties() throws ParseException { setupData("testSaveProperties"); - persist(); + persist(newServer()); checkTables("testSaveProperties", "properties"); } @Test public void testUpdateExistingProperties() throws ParseException { setupData("testUpdateExistingProperties"); - persist(); + persist(newServer()); checkTables("testUpdateExistingProperties", "properties"); } - private void persist() throws ParseException { - TimeZone initialZone = TimeZone.getDefault(); - try { - TimeZone.setDefault(TimeZone.getTimeZone("GMT")); - Date date = new SimpleDateFormat("yyyy-MM-dd HH:mm").parse("2010-05-18 17:59"); - Server server = new ServerImpl("123", "2.2", date); - ServerMetadataPersister persister = new ServerMetadataPersister(server, getSession()); - persister.start(); - } finally { - TimeZone.setDefault(initialZone); - } + @Test + public void testDeleteProperties() throws ParseException { + setupData("testDeleteProperties"); + Server server = mock(Server.class); + when(server.getStartedAt()).thenReturn(new SimpleDateFormat("yyyy-MM-dd HH:mm").parse("2010-05-18 17:59"));//this is a mandatory not-null property + persist(server); + checkTables("testDeleteProperties", "properties"); + } + + private void persist(Server server) { + ServerMetadataPersister persister = new ServerMetadataPersister(server, getSession()); + persister.start(); + } + + private Server newServer() throws ParseException { + Date date = new SimpleDateFormat("yyyy-MM-dd HH:mm").parse("2010-05-18 17:59"); + Server server = mock(Server.class); + when(server.getPermanentServerId()).thenReturn("1abcdef"); + when(server.getId()).thenReturn("123"); + when(server.getVersion()).thenReturn("2.2"); + when(server.getStartedAt()).thenReturn(date); + + return server; + } } diff --git a/sonar-server/src/test/resources/org/sonar/server/platform/ServerImplTest/shouldLoadServerIdFromDatabase.xml b/sonar-server/src/test/resources/org/sonar/server/platform/ServerImplTest/shouldLoadServerIdFromDatabase.xml new file mode 100644 index 00000000000..6e0919bd266 --- /dev/null +++ b/sonar-server/src/test/resources/org/sonar/server/platform/ServerImplTest/shouldLoadServerIdFromDatabase.xml @@ -0,0 +1,6 @@ +<dataset> + + <properties id="1" prop_key="sonar.server_id" text_value="abcde" resource_id="[null]" user_id="[null]"/> + <properties id="2" prop_key="sonar.core.serverBaseURL" text_value="http://192.168.0.1" resource_id="[null]" user_id="[null]"/> + +</dataset>
\ No newline at end of file diff --git a/sonar-server/src/test/resources/org/sonar/server/plugins/ServerPluginRepositoryTest/sonar-artifact-size-plugin-0.2.jar b/sonar-server/src/test/resources/org/sonar/server/plugins/DefaultServerPluginRepositoryTest/sonar-artifact-size-plugin-0.2.jar Binary files differindex 19533234582..19533234582 100644 --- a/sonar-server/src/test/resources/org/sonar/server/plugins/ServerPluginRepositoryTest/sonar-artifact-size-plugin-0.2.jar +++ b/sonar-server/src/test/resources/org/sonar/server/plugins/DefaultServerPluginRepositoryTest/sonar-artifact-size-plugin-0.2.jar diff --git a/sonar-server/src/test/resources/org/sonar/server/startup/ServerMetadataPersisterTest/testDeleteProperties-result.xml b/sonar-server/src/test/resources/org/sonar/server/startup/ServerMetadataPersisterTest/testDeleteProperties-result.xml new file mode 100644 index 00000000000..f7ada567379 --- /dev/null +++ b/sonar-server/src/test/resources/org/sonar/server/startup/ServerMetadataPersisterTest/testDeleteProperties-result.xml @@ -0,0 +1,7 @@ +<dataset> + + <properties id="1" prop_key="other" resource_id="[null]" text_value="some text" user_id="[null]"/> + + <!-- not null property --> + <properties id="4" prop_key="sonar.core.startTime" resource_id="[null]" text_value="2010-05-18T17:59:00+0000" user_id="[null]"/> +</dataset>
\ No newline at end of file diff --git a/sonar-server/src/test/resources/org/sonar/server/startup/ServerMetadataPersisterTest/testDeleteProperties.xml b/sonar-server/src/test/resources/org/sonar/server/startup/ServerMetadataPersisterTest/testDeleteProperties.xml new file mode 100644 index 00000000000..ec292a1388d --- /dev/null +++ b/sonar-server/src/test/resources/org/sonar/server/startup/ServerMetadataPersisterTest/testDeleteProperties.xml @@ -0,0 +1,9 @@ +<dataset> + + <properties id="1" prop_key="other" resource_id="[null]" text_value="some text" user_id="[null]" /> + + <properties id="2" prop_key="sonar.core.id" resource_id="[null]" text_value="123" user_id="[null]"/> + <properties id="3" prop_key="sonar.core.version" resource_id="[null]" text_value="2.2" user_id="[null]"/> + <properties id="4" prop_key="sonar.core.startTime" resource_id="[null]" text_value="2010-05-18T17:59:00+0000" user_id="[null]"/> + +</dataset>
\ No newline at end of file |