22 mayo 2016

Maven: renombrar masivamente properties de versiones de dependencias en POM

Supongamos que tenemos un pom.xml de Maven en donde usamos properties para definir las versiones de nuestras dependencias y luego las referenciamos con placeholders ${}:
<?xml version="1.0"?>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
 <modelVersion>4.0.0</modelVersion>

 <groupId>ar.com.kamikazesoftware</groupId>
 <artifactId>test</artifactId>
 <version>1.0-SNAPSHOT</version>

 <properties>
  <version.commons-lang3>3.4</version.commons-lang3>
  <version.commons-io>2.4</version.commons-io>
  <!-- más definiciones similares (version.x) -->
 </properties>

 <dependencies>
  <dependency>
   <groupId>org.apache.commons</groupId>
   <artifactId>commons-lang3</artifactId>
   <version>${version.commons-lang3}</version>
  </dependency>
  <dependency>
   <groupId>commons-io</groupId>
   <artifactId>commons-io</artifactId>
   <version>${version.commons-io}</version>
  </dependency>

  <!-- más definiciones de dependencias similares, usando <version>${version.x}</version> -->
y queremos invertir el orden del prefijo version y el nombre de la dependencia, queremos obtener:
<?xml version="1.0"?>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
 <modelVersion>4.0.0</modelVersion>

 <groupId>ar.com.kamikazesoftware</groupId>
 <artifactId>test</artifactId>
 <version>1.0-SNAPSHOT</version>

 <properties>
  <commons-lang3.version>3.4</commons-lang3.version>
  <commons-io.version>2.4</commons-io.version>
  <!-- más definiciones similares (version.x) -->
 </properties>

 <dependencies>
  <dependency>
   <groupId>org.apache.commons</groupId>
   <artifactId>commons-lang3</artifactId>
   <version>${commons-lang3.version}</version>
  </dependency>
  <dependency>
   <groupId>commons-io</groupId>
   <artifactId>commons-io</artifactId>
   <version>${commons-io.version}</version>
  </dependency>

  <!-- más definiciones de dependencias similares, usando <version>${version.x}</version> -->
Si sólo tenemos unas pocas definiciones podemos buscar y reemplazar individualmente los nombres con un editor de texto, pero esto se vuelve poco práctico si tenemos decenas de definiciones. Podemos entonces usar sed para reemplazar todas las apariciones del patrón version.x por x.version.
# reemplazar  con 
sed -e 's/\(]*\)>\)/<\2.version>/' -i pom.xml

# reemplazar  con 
sed -e 's/\(<\/version\.\([^>]*\)>\)/<\/\2.version>/' -i pom.xml

# reemplazar ${version.algo} con ${algo.version}
sed -e 's/\${version\.\([^}]*\)}/\${\1.version}/' -i pom.xml

20 julio 2015

XSLT: mover un atributo dado de un elemento XML a otro

Supongamos que tenemos el siguiente XML, llamemos al archivo order.xml:

<Order id="0001">
    <Customer id="1">Juan Perez</Customer>
</Order>

Y queremos mover el atributo "id" del elemento "Order" a un nuevo elemento "Extension", llamemos al archivo resultante order.out.xml:

<Order>
   <Customer id="1">Juan Perez</Customer>
   <Extension id="0001"/>
</Order>

Para obtener la transformación deseada, ejecutamos la siguiente XSLT, llamémosla move_id_attribute_under_extension_element.xsl:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

    <xsl:output omit-xml-declaration="no" indent="yes" />

    <!-- copiar todos los nodos excepto el nodo Order -->    
    <xsl:template match="node()|@*" name="identity">
        <xsl:copy>
            <xsl:apply-templates select="node()|@*" />
        </xsl:copy>
    </xsl:template>

    <!-- cuando se encuentra el elemento Order -->
    <xsl:template match="Order">
        <!-- copiar el elemento Order, con modificaciones -->
        <Order>
            <!-- copiar todos los nodos que se encuentran dentro de Order excepto el atributo id, que se mueve a <Extension id="..."> -->
            <xsl:apply-templates select="node()|@*[local-name() != 'id' ]" />
            <!-- mover el atributo id del elemento Order a un elemento nuevo Extension -->
            <Extension>
                <xsl:attribute name="id">
                    <xsl:value-of select="./@id"/>
                </xsl:attribute>
            </Extension>
        </Order>
    </xsl:template>
</xsl:stylesheet>

Para ejecutar la XSLT desde Ubuntu:

$ saxon-xslt -o order.out.xml order.xml move_id_attribute_under_extension_element.xsl

Para instalar saxon-xslt en Ubuntu:

$ sudo apt-get install libsaxon-java


31 marzo 2011

Empaquetar una aplicación de consola con Maven

Veremos cómo configurar el POM de un proyecto de una aplicación de consola para obtener un archivo zip conteniendo la aplicación y todas sus dependencias (JARs), y un directorio con los archivos de configuración de la aplicación.

Llamaremos a nuestro proyecto console-poc, que va a ser también el artifactId del POM.

Estructura de directorios


Vamos a agregar algunos directorios a la estructura de directorios de nuestro proyecto que es la standard de Maven.

Agregaremos src/main/bin, para contener los scripts de ejecución de la aplicación y src/main/config para contener los archivos de configuración:

src/
|-- main
|   |-- bin
|   |   |-- run.cmd
|   |   `-- run.sh
|   |-- config
|   |   |-- application.properties
|   |   |-- applicationContext.xml
|   |   `-- log4j.xml
|   `-- java
|       `-- com
...


JAR plugin: agregar el directorio src/main/config al Manifest


Configuramos el plugin de JAR en el POM para que agregue el directorio src/main/config al classpath definido en el Manifest de la aplicación.

<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<!-- esto hace que al crear el jar se nombre console-poc.jar en vez de console-poc-1.0.0.jar si la versión del artifact es 1.0.0 -->
<finalName>${artifactId}</finalName>
<archive>
<manifest>
<!-- agregar todos los .jar de las dependencias en el classpath del Manifest -->
<addClasspath>true</addClasspath>
<!-- vamos a agregar antes del nombre de cada .jar en el classpath del Manifest el prefijo lib/ -->
<!-- porque luego al construir el assembly copiaremos los .jar de las dependencias al directorio lib/-->
<classpathPrefix>lib/</classpathPrefix>
<!-- la clase que se ejecuta cuando se ejecute "java -jar console-poc.jar" -->
<mainClass>ar.com.kamikazesoftware.poc.console.App</mainClass>
</manifest>
<manifestEntries>
<!-- además de los nombres de las dependencias, agregar el directorio config al classpath del Manifest -->
<Class-Path>config/</Class-Path>
</manifestEntries>
</archive>
</configuration>
</plugin>


Assembly Plugin


Configuramos el plugin de Assembly para usar un descriptor llamado distribution.xml que el plugin tomará del directorio src/main/assembly:

<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<configuration> 
<descriptors>
<!-- el descriptor contiene la información para armar el assembly -->
<descriptor>src/main/assembly/distribution.xml</descriptor>
</descriptors>
</configuration>
<executions>
<!-- ejecutar el goal 'single' del plugin de assembly al ejecutar mvn package -->
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>    
</plugin>


En execution se define que se ejecute el goal 'single' del plugin de assembly cuando se ejecute 'mvn package' desde el directorio de nuestro proyecto, es decir que construya el assembly cuando se ejecute 'mvn package'.

A continuación el descriptor distribution.xml comentado:

<assembly>
<id>distribution</id>

<!-- el formato de salida es archivo .zip -->
<formats>
<format>zip</format>
</formats>

<!-- incluir todas las dependencias runtime en el directorio lib del archivo .zip de salida-->
<dependencySets>
<dependencySet>
<outputDirectory>/lib</outputDirectory>
<useProjectArtifact>false</useProjectArtifact>
</dependencySet>
</dependencySets>

<fileSets>
<!-- incluir el jar de la aplicación en el directorio base del archivo .zip -->
<fileSet>
<directory>target</directory>
<outputDirectory></outputDirectory>
<includes>
<include>*.jar</include>
</includes>
</fileSet>

<!-- copiar los scripts al directorio base del archivo .zip -->
<fileSet>
<directory>src/main/bin</directory>
<outputDirectory></outputDirectory>
<includes>
<include>run.cmd</include>
<include>run.sh</include>
</includes>
</fileSet>

<!-- copiar los archivos de configuración al directorio config del archivo .zip -->
<fileSet>
<directory>src/main/config</directory>
<outputDirectory>/config</outputDirectory>
<includes>
<include>**/**</include>
</includes>
</fileSet>
</fileSets>
</assembly>


Script para ejecutar la aplicación desde linea de comandos



El script run.sh (y run.cmd la versión Windows), contiene la siguiente linea que ejecuta la máquina virtual de java parametrizada para ejecutar la aplicación:

java -jar console-poc.jar


Ensamblado


Con esta configuración ejecutamos

mvn clean package


en una terminal, parados en el directorio de nuestro proyecto, y en el directorio target se creará un archivo console-poc-0.0.1-SNAPSHOT.zip con el assembly.

Ver también


En una futura ocasión hablaremos de otro plugin para armar paquetes, el
Appassembler plugin que entre otras cosas arma el script de ejecución automaticamente.

27 enero 2011

HSQL en modo server en JBoss AS

El propósito de este artículo es mostrar cómo habilitar la instancia de HSQL que corre en JBoss AS para que sea una instancia en modo server. El modo server de HSQL permite acceder a la base a través de clientes JDBC, lo cual puede ser útil para llevar a cabo acciones sobre la base de datos y sus tablas con una herramienta como Squirrel.

Convenciones


  • $JBOSS_HOME es el directorio donde está instalado JBoss.
  • $JBOSS_HOST es la dirección ip o host donde está instalado JBoss
  • La configuración de JBoss que vamos a modificar es la default.

Habilitar Binding


En $JBOSS_HOME/server/default/conf/bindingservice.beans/META-INF/bindings-jboss-beans.xml, descomentar:

<bean class="org.jboss.services.binding.ServiceBindingMetadata">
 <property name="serviceName">jboss:service=Hypersonic</property>
 <property name="port">1701</property>
 <property name="description">TCP/IP socket for remote connection to Hypersonic database</property>
</bean>

Data Source


Modificar el archivo $JBOSS_HOME/server/default/deploy/hsqldb-ds.xml para ejecutar HSQL en modo server. El archivo que viene con JBoss tiene la configuración para este fin comentada, sólo hay que descomentar lo que se necesite y comentar lo que se deja de usar. Aqui muestro cómo quedaría:

<?xml version="1.0" encoding="UTF-8"?>

<!-- The Hypersonic embedded database JCA connection factory config -->

<!-- See http://www.jboss.org/community/wiki/Multiple1PC for information about local-tx-datasource -->
<!-- $Id: hsqldb-ds.xml 97536 2009-12-08 14:05:07Z jesper.pedersen $ -->

<datasources>
<local-tx-datasource>
<!-- The jndi name of the DataSource, it is prefixed with java:/ -->
<!-- Datasources are not available outside the virtual machine -->
<jndi-name>DefaultDS</jndi-name>

<!-- For server mode db, allowing other processes to use hsqldb over tcp.
This requires the org.jboss.jdbc.HypersonicDatabase mbean. -->
<connection-url>jdbc:hsqldb:hsql://${jboss.bind.address}:1701</connection-url>

<!-- The driver class -->
<driver-class>org.hsqldb.jdbcDriver</driver-class>

<!-- The login and password -->
<user-name>sa</user-name>
<password></password>

<!--example of how to specify class that determines if exception means connection should be destroyed-->
<!--exception-sorter-class-name>org.jboss.resource.adapter.jdbc.vendor.DummyExceptionSorter</exception-sorter-class-name-->

<!-- this will be run before a managed connection is removed from the pool for use by a client-->
<!--<check-valid-connection-sql>select * from something</check-valid-connection-sql> -->

<!-- The minimum connections in a pool/sub-pool. Pools are lazily constructed on first use -->
<min-pool-size>5</min-pool-size>

<!-- The maximum connections in a pool/sub-pool -->
<max-pool-size>20</max-pool-size>

<!-- The time before an unused connection is destroyed -->
<!-- NOTE: This is the check period. It will be destroyed somewhere between 1x and 2x this timeout after last use -->
<!-- TEMPORARY FIX! - Disable idle connection removal, HSQLDB has a problem with not reaping threads on closed connections -->
<idle-timeout-minutes>0</idle-timeout-minutes>

<!-- sql to call when connection is created
<new-connection-sql>some arbitrary sql</new-connection-sql>
-->

<!-- sql to call on an existing pooled connection when it is obtained from pool 
<check-valid-connection-sql>some arbitrary sql</check-valid-connection-sql>
-->

<!-- example of how to specify a class that determines a connection is valid before it is handed out from the pool
<valid-connection-checker-class-name>org.jboss.resource.adapter.jdbc.vendor.DummyValidConnectionChecker</valid-connection-checker-class-name>
-->

<!-- Whether to check all statements are closed when the connection is returned to the pool,
this is a debugging feature that should be turned off in production -->
<track-statements/>

<!-- Use the getConnection(user, pw) for logins
<application-managed-security/>
-->

<!-- Use the security domain defined in conf/login-config.xml -->
<security-domain>HsqlDbRealm</security-domain>

<!-- Use the security domain defined in conf/login-config.xml or the
getConnection(user, pw) for logins. The security domain takes precedence.
<security-domain-and-application>HsqlDbRealm</security-domain-and-application>
-->

<!-- HSQL DB benefits from prepared statement caching -->
<prepared-statement-cache-size>32</prepared-statement-cache-size>

<!-- corresponding type-mapping in the standardjbosscmp-jdbc.xml (optional) -->
<metadata>
<type-mapping>Hypersonic SQL</type-mapping>
</metadata>

<!-- Uncomment when using hsqldb in server mode -->
<depends>jboss:service=Hypersonic</depends>
</local-tx-datasource>

<!-- Uncomment if you want hsqldb accessed over tcp (server mode) -->
<mbean code="org.jboss.jdbc.HypersonicDatabase" name="jboss:service=Hypersonic">
<attribute name="Port">        
<value-factory bean="ServiceBindingManager" method="getIntBinding"  parameter="jboss:service=Hypersonic"/>
</attribute>
<attribute name="BindAddress">        
<value-factory bean="ServiceBindingManager" method="getStringBinding" parameter="jboss:service=Hypersonic"/>
</attribute>     
<attribute name="Silent">true</attribute>
<attribute name="Database">default</attribute>
<attribute name="Trace">false</attribute>
<attribute name="No_system_exit">true</attribute>
</mbean>
</datasources>

Cliente


Luego se puede acceder con un cliente configurándolo de la siguiente forma:

Driver Class: org.hsqldb.jdbcDriver (para usar esta clase incluír en el classpath del cliente $JBOSS_HOME/common/lib/hsqldb.jar)
URL: jdbc:hsqldb:hsql://$JBOSS_HOST:1701 (si JBoss estuviera instalado localmente entonces quedaría jdbc:hsqldb:hsql://127.0.0.1:1701)
Username: sa
Password:

07 diciembre 2010

Spring BeanPostProcessor, ServletContextAware y ServletContext

En este post muestro cómo definir un componente para configurar un bean de Spring que necesita configuración que no se puede setear rígida en un parámetro de la aplicación. En este caso, necesito saber el path absoluto del directorio de contextos del servlet container (el directorio webapps de Tomcat), y si la aplicación se deploya en distintos sistemas operativos ese path puede cambiar de un sistema operativo al otro.

BeanPostProcessor


Defino una clase que implementa la interface BeanPostProcessor, que provee dos métodos:

Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException;

Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException;


La clase es DataDistributionServiceBeanPostProcessor:

package ar.com.kamikazesoftware.console.context;

import java.io.File;

import javax.servlet.ServletContext;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.beans.factory.config.PropertyPlaceholderConfigurer;
import org.springframework.web.context.ServletContextAware;

import ar.com.kamikazesoftware.services.dataDistribution.DataDistributionService;

/**
* {@link BeanPostProcessor} to set the {@link DataDistributionService}'s xmlFilesRootFolder property
* as an absolute path including the servlet container's contexts directory followed by 
* the relative path set by the user in the application's resource bundle parameter called "xmlFilesRootFolder".
* 
* The bean dataDistributionService has injected the xmlFilesRootFolder parameter taking it from the Spring's
* {@link PropertyPlaceholderConfigurer} configured with the application.properties file as the resource bundle:
* 
*  <bean id="dataDistributionService"
*  class="ar.com.kamikazesoftware.services.dataDistribution.local.LocalDataDistributionService">
*  <property name="xmlFilesRootFolder"><value>${xmlFilesRootFolder}</value></property>
*  ...
*
* 
* So before using the original xmlFilesRootFolder, that can't be useful in such environments like Linux
* because the relative path doesn't get set relative to servlet container's contexts directory,
* this BeanPostProcessor sets the xmlFilesRootFolder concatenating before it the absolute servlet container's contexts directory.
* 
* For example, if in application.properties exists this line:
* 
* xmlFilesRootFolder=webapps/ROOT/players/
* 
* And Tomcat is installed on c:/tools/tomcat
* 
* then this BeanPostProcessor sets the DataDistributionService xmlFilesRootFolder property with the value:
* 
* c:/tools/tomcat/webapps/ROOT/players
* 
* @author EMenendez
*
*/
public class DataDistributionServiceBeanPostProcessor implements BeanPostProcessor, ServletContextAware {

private static final Log log = LogFactory.getLog(DataDistributionServiceBeanPostProcessor.class);

private String containerRootDirectoryPath;

@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
// do nothing before initialization
return bean;
}

@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
// apply only for DataDistributionService
if (bean instanceof DataDistributionService) {
if (log.isDebugEnabled())
log.debug("Executing postProcessAfterInitialization for bean " + beanName);

DataDistributionService dataDistributionService = (DataDistributionService) bean;

// TODO try use reflection to call getter and setter for XmlFilesRootFolder property, and remove getter and setter from the interface DataDistributionService
String relativeControlFileDirectory = dataDistributionService.getXmlFilesRootFolder();

if (log.isDebugEnabled())
log.debug("DataDistributionService's xmlFilesRootFolder from resource bundle (relative path): " + relativeControlFileDirectory);

String absoluteControlFileDirectory = containerRootDirectoryPath + relativeControlFileDirectory;

dataDistributionService.setXmlFilesRootFolder(absoluteControlFileDirectory);

if (log.isDebugEnabled())
log.debug("DataDistributionService's xmlFilesRootFolder set to absolute path: " + absoluteControlFileDirectory);

return dataDistributionService;
} else {
// Do nothing if the processed bean is not DataDistributionService
return bean;
}
}

@Override
public void setServletContext(ServletContext servletContext) {
// get tomcat/webapps/starmount-console/ dir
String contextRootDirectoryPath = servletContext.getRealPath("/");
File contextRootDirectory = new File(contextRootDirectoryPath);

// get tomcat/webapps/ dir
containerRootDirectoryPath = contextRootDirectory.getParent() + System.getProperty("file.separator");

if (log.isDebugEnabled())
log.debug("Initializing DataDistributionServiceBeanPostProcessor. Set containerRootDirectoryPath to: " + containerRootDirectoryPath);  
}
}


Application Context


Y en applicationContext.xml de Spring están definidos el bean y su post processor:

<bean id="dataDistributionService"
class="ar.com.kamikazesoftware.services.dataDistribution.local.LocalDataDistributionService">
<property name="xmlFilesRootFolder"><value>${xmlFilesRootFolder}</value></property>
<constructor-arg index="0" ref="SceneDAO" />  
</bean> 

<bean class="ar.com.kamikazesoftware.console.context.DataDistributionServiceBeanPostProcessor"/>


ServletContextAware



Como DataDistributionServiceBeanPostProcessor implementa también la interface ServletContextAware, cuando Spring inicializa el post processor ejecuta el método:

public void setServletContext(ServletContext servletContext)


que es el que usamos para obtener el path local donde están guardadas las webapps en el servlet container (Tomcat):

// get tomcat/webapps/myapp/ dir
String contextRootDirectoryPath = servletContext.getRealPath("/");
File contextRootDirectory = new File(contextRootDirectoryPath);
// get tomcat/webapps/ dir
containerRootDirectoryPath = contextRootDirectory.getParent();


De esta forma amigos termina este ejemplo de implementación y uso de BeanPostProcessor, ServletContextAware y ServletContext.

Hasta la próxima!

23 junio 2008

DBCP: Validar las conexiones del pool

MySQL cierra las conexiones que han estado idle por más de un determinado tiempo (default 8 hs.), por lo tanto cuando no se accede a la aplicación por un tiempo, las conexiones de su pool quedan idle y luego MySQL las cierra. Luego, cuando la aplicación intenta usar alguna conexión del pool, tira la exception Broken pipe.

Validación de conexiones


Para evitarlo, se pueden agregar un par de parámetros al data source de la aplicación, que está configurado con DBCP.

validationQuery


Para validar una conexión es necesario setear este parámetro, que tiene que ser una query SQL que devuelva 1 o más filas, que depende del motor de base de datos al que se esté accediendo. En el caso de MySQL una query trivial es SELECT 1 (en Oracle por ej. sería SELECT 1 FROM DUAL).

testOnBorrow


El parámetro testOnBorrow provoca que se verifique que la conexión esté abierta cuando se solicite al pool, antes de usarla. Si la conexión no está abierta el pool la descarta e intenta obtener otra.
La siguiente porción de configuración, es para un data source configurado en Spring. La presento simplemente para ilustrar el seteo de los parámetros de DBCP.

<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
 <property name="driverClassName" value="${jdbc.driverClassName}"/>
 <property name="url" value="${jdbc.url}"/>
 <property name="username" value="${jdbc.username}"/>
 <property name="password" value="${jdbc.password}"/>
 <property name="validationQuery">
  <value>SELECT 1</value>
 </property>
 <property name="testOnBorrow">
  <value>true</value>
 </property>
</bean>


testWhileIdle


El parámetro testWhileIdle valida las conexiones cuando éstas se encuentran Idle.

<property name="validationQuery">
 <value>SELECT 1</value>
</property>
<property name="testWhileIdle">
 <value>true</value>
</property>
<property name="timeBetweenEvictionRunsMillis">
 <value>60000</value>
</property>
<property name="numTestsPerEvictionRun">
 <value>3</value>
</property>
<property name="minEvictableIdleTimeMillis">
 <value>7200000</value>
</property>


Links


http://commons.apache.org/dbcp/
http://commons.apache.org/dbcp/configuration.html
post en mailing list de dbpc

03 junio 2008

SQL: información que no está en una relación usando LEFT JOIN

Dada la siguiente estructura, dos tablas de entidad y una de relación (sintaxis de MySQL):

create table alumno (id integer, nombre varchar(255)) primary key (id);
create table materia (id integer, nombre varchar(255)) primary key (id);
create table inscripcion(materia_id integer, alumno_id integer) primary key (materia_id, alumno_id);

Queremos conocer las materias en las cuales no hay inscripto ningún alumno.

Inscribimos a Emiliano en Matemática, a Luis en Lengua, y a Martin en ninguna materia. Por lo tanto nadie está inscripto en Gimnasia

insert into alumno (id, nombre) values (1, 'Emiliano');
insert into alumno (id, nombre) values (2, 'Luis');
insert into alumno (id, nombre) values (3, 'Martin');

insert into materia (id, nombre) values (1, 'Matemática');
insert into materia (id, nombre) values (2, 'Lengua');
insert into materia (id, nombre) values (3, 'Gimnasia');

insert into inscripcion (materia_id, alumno_id) values (1, 1);
insert into inscripcion (materia_id, alumno_id) values (2, 2);

Ejecutamos la query

SELECT m.nombre
FROM materia m
LEFT JOIN inscripcion i ON m.id = i.materia_id
WHERE i.materia_id is null;

y resulta en

+-----------+
| nombre |
+-----------+
| Gimnasia |
+-----------+