/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.isis.applib;
import java.io.UnsupportedEncodingException;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLDecoder;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.jdo.annotations.PersistenceCapable;
import com.google.common.collect.Lists;
import org.reflections.vfs.SystemDir;
import org.reflections.vfs.Vfs;
import org.apache.isis.applib.annotation.DomainService;
import org.apache.isis.applib.fixturescripts.FixtureScript;
/**
* Programmatic specification of the constituent parts of an application, most specifically the modules that contain
* domain services and possibly entities.
*
* <p>
* The modules are specified as classes; the framework uses the packages of these classes as the locations on the
* classpath to search for domain services (annotated with {@link org.apache.isis.applib.annotation.DomainService})
* and entities (annotated with {@link PersistenceCapable}).
* </p>
*
* <p>
* The interface also allows certain other aspects of the runtime to be specified:
* <ul>
* <li>which authentication and/or authorization mechanism to be used (useful to disable security when developing/demoing)</li>
* <li>which fixtures to run by default</li>
* <li>overriding of arbitrary other configuration properties</li>
* <li>which modules to load (useful when developing, to reduce startup times; just load the modules required)</li>
* </ul>
* </p>
*
* <p>
* To use, implement this interface and then define as the <tt>isis.appManifest</tt> key within the
* <tt>isis.properties</tt> configuration file, or specify programmatically when running integration tests.
* </p>
*
* <p>
* By convention the class implementing this interface reside in a <tt>xxx-home</tt> Maven module. This
* can be referenced by both the <tt>xxx-integtests</tt> (Maven) module and the <tt>xxx-webapp</tt> (Maven) module,
* allowing configuration to be centralized.
* </p>
*
* <p>
* There are a number of subsidiary goals (not yet implemented):
* <ul>
* <li>
* <p>Allow different integration tests to run with different manifests. Normally the running application is
* shared (on a thread-local) between integration tests. What the framework could do is to be intelligent
* enough to keep track of the manifest in use for each integration test and tear down
* the shared state if the "next" test uses a different manifest</p>
* </li>
* <li>
* Speed up bootstrapping by only scanning for classes annotated by
* {@link org.apache.isis.applib.annotation.DomainService} and {@link javax.jdo.annotations.PersistenceCapable}
* once.
* </li>
* <li>
* Provide a programmatic way to contribute elements of `web.xml`.
* </li>
* <li>
* Provide a programmatic way to configure Shiro security.
* </li>
* <li>
* <p>Anticipate the module changes forthcoming in Java 9. Eventually we see that the AppManifest class acting
* as an "aggregator", with the list of modules will become Java 9 modules each advertising the types that they
* export. It might even be possible for AppManifests to be switched on and off dynamically (eg if Java9 is
* compatible with OSGi, being one of the design goals).</p>
* </li>
* </ul>
* </p>
*/
public interface AppManifest {
/**
* A list of classes, each of which representing the root of one of the modules containing services and possibly
* entities, which together makes up the running application.
*
* <p>
* The package of each such class is used as the basis for searching for domain services and registered
* entities. As such it replaces and overrides both the
* <tt>isis.services.ServicesInstallerFromAnnotation.packagePrefix</tt> key (usually found in the
* <tt>isis.properties</tt> file) and the
* <tt>isis.persistor.datanucleus.RegisterEntities.packagePrefix</tt> key (usually found in the
* <tt>persistor_datanucleus.properties</tt> file). The value of the <tt>isis.services-installer</tt>
* configuration property is also ignored.
* </p>
*/
public List<Class<?>> getModules();
/**
* If non-null, overrides the value of <tt>isis.services</tt> configuration property to specify a list of
* additional classes to be instantiated as domain services (over and above the {@link DomainService}-annotated
* services defined via {@link #getModules()}.
*
* <p>
* Normally we recommend services are defined exclusively through {@link #getModules()}, and that this method
* should therefore return an empty list. However, this method exists to support those use cases where either the
* service required does not have a {@link DomainService} annotation, or where it does have the annotation
* but its containing module cannot (for whatever reason) be listed under {@link #getModules()}.
* </p>
*/
public List<Class<?>> getAdditionalServices();
/**
* If non-null, overrides the value of <tt>isis.authentication</tt> configuration property to specify the
* authentication mechanism.
*
* <p>
* Ignored for integration tests (which always uses the 'bypass' mechanism).
* </p>
*/
public String getAuthenticationMechanism();
/**
* If non-null, overrides the value of <tt>isis.authorization</tt> configuration property.
*
* <p>
* Ignored for integration tests (which always uses the 'bypass' mechanism).
* </p>
*/
public String getAuthorizationMechanism();
/**
* If non-null, overrides the value of <tt>isis.fixtures</tt> configuration property.
*/
public List<Class<? extends FixtureScript>> getFixtures();
/**
* Overrides for any other configuration properties.
*/
public Map<String,String> getConfigurationProperties();
/**
* Holds the set of domain services, persistent entities and fixture scripts.services
*/
public static class Registry {
public final static List<String> FRAMEWORK_PROVIDED_SERVICES = Collections.unmodifiableList(Arrays.asList(
"org.apache.isis.applib",
"org.apache.isis.core.wrapper" ,
"org.apache.isis.core.metamodel.services" ,
"org.apache.isis.core.runtime.services" ,
"org.apache.isis.schema.services" ,
"org.apache.isis.objectstore.jdo.applib.service" ,
"org.apache.isis.viewer.restfulobjects.rendering.service" ,
"org.apache.isis.objectstore.jdo.datanucleus.service.support" ,
"org.apache.isis.objectstore.jdo.datanucleus.service.eventbus" ,
"org.apache.isis.viewer.wicket.viewer.services"));
private static Registry instance = new Registry();
public static Registry instance() {
return instance;
}
//region > persistenceCapableTypes
private Set<Class<?>> persistenceCapableTypes;
/**
* @return <tt>null</tt> if no appManifest is defined
*/
public Set<Class<?>> getPersistenceCapableTypes() {
return persistenceCapableTypes;
}
public void setPersistenceCapableTypes(final Set<Class<?>> persistenceCapableTypes) {
this.persistenceCapableTypes = persistenceCapableTypes;
}
//endregion
//region > mixinTypes
private Set<Class<?>> mixinTypes;
/**
* @return <tt>null</tt> if no appManifest is defined
*/
public Set<Class<?>> getMixinTypes() {
return mixinTypes;
}
public void setMixinTypes(final Set<Class<?>> mixinTypes) {
this.mixinTypes = mixinTypes;
}
//endregion
//region > fixtureScriptTypes
private Set<Class<? extends FixtureScript>> fixtureScriptTypes;
/**
* @return <tt>null</tt> if no appManifest is defined
*/
public Set<Class<? extends FixtureScript>> getFixtureScriptTypes() {
return fixtureScriptTypes;
}
public void setFixtureScriptTypes(final Set<Class<? extends FixtureScript>> fixtureScriptTypes) {
this.fixtureScriptTypes = fixtureScriptTypes;
}
//endregion
//region > domainServiceTypes
private Set<Class<?>> domainServiceTypes;
/**
* @return <tt>null</tt> if no appManifest is defined
*/
public Set<Class<?>> getDomainServiceTypes() {
return domainServiceTypes;
}
public void setDomainServiceTypes(final Set<Class<?>> domainServiceTypes) {
this.domainServiceTypes = domainServiceTypes;
}
//endregion
//region > urlTypes
public List<Vfs.UrlType> getUrlTypes() {
final List<Vfs.UrlType> urlTypes = Lists.newArrayList();
urlTypes.add(new EmptyIfFileEndingsUrlType(".pom", ".jnilib", "QTJava.zip"));
urlTypes.add(new JettyConsoleUrlType());
urlTypes.addAll(Arrays.asList(Vfs.DefaultUrlTypes.values()));
return urlTypes;
}
//endregion
private static class EmptyIfFileEndingsUrlType implements Vfs.UrlType {
private final List<String> fileEndings;
private EmptyIfFileEndingsUrlType(final String... fileEndings) {
this.fileEndings = Lists.newArrayList(fileEndings);
}
public boolean matches(URL url) {
final String protocol = url.getProtocol();
final String externalForm = url.toExternalForm();
if (!protocol.equals("file")) {
return false;
}
for (String fileEnding : fileEndings) {
if (externalForm.endsWith(fileEnding))
return true;
}
return false;
}
public Vfs.Dir createDir(final URL url) throws Exception {
return emptyVfsDir(url);
}
private static Vfs.Dir emptyVfsDir(final URL url) {
return new Vfs.Dir() {
@Override
public String getPath() {
return url.toExternalForm();
}
@Override
public Iterable<Vfs.File> getFiles() {
return Collections.emptyList();
}
@Override
public void close() {
//
}
};
}
}
private static class JettyConsoleUrlType implements Vfs.UrlType {
public boolean matches(URL url) {
final String protocol = url.getProtocol();
final String externalForm = url.toExternalForm();
final boolean matches = protocol.equals("file") && externalForm.contains("jetty-console") && externalForm.contains("-any-") && externalForm.endsWith("webapp/WEB-INF/classes/");
return matches;
}
public Vfs.Dir createDir(final URL url) throws Exception {
return new SystemDir(getFile(url));
}
/**
* try to get {@link java.io.File} from url
*
* <p>
* Copied from {@link Vfs} (not publicly accessible)
* </p>
*/
static java.io.File getFile(URL url) {
java.io.File file;
String path;
try {
path = url.toURI().getSchemeSpecificPart();
if ((file = new java.io.File(path)).exists()) return file;
} catch (URISyntaxException e) {
}
try {
path = URLDecoder.decode(url.getPath(), "UTF-8");
if (path.contains(".jar!")) path = path.substring(0, path.lastIndexOf(".jar!") + ".jar".length());
if ((file = new java.io.File(path)).exists()) return file;
} catch (UnsupportedEncodingException e) {
}
try {
path = url.toExternalForm();
if (path.startsWith("jar:")) path = path.substring("jar:".length());
if (path.startsWith("file:")) path = path.substring("file:".length());
if (path.contains(".jar!")) path = path.substring(0, path.indexOf(".jar!") + ".jar".length());
if ((file = new java.io.File(path)).exists()) return file;
path = path.replace("%20", " ");
if ((file = new java.io.File(path)).exists()) return file;
} catch (Exception e) {
}
return null;
}
}
}
public static class Util {
public static final String ISIS_PERSISTOR = "isis.persistor.";
public static final String ISIS_PERSISTOR_DATANUCLEUS = ISIS_PERSISTOR + "datanucleus.";
public static final String ISIS_PERSISTOR_DATANUCLEUS_IMPL = ISIS_PERSISTOR_DATANUCLEUS + "impl.";
public static Map<String,String> withJavaxJdoRunInMemoryProperties(final Map<String, String> map) {
map.put(ISIS_PERSISTOR_DATANUCLEUS_IMPL + "javax.jdo.option.ConnectionURL", "jdbc:hsqldb:mem:test");
map.put(ISIS_PERSISTOR_DATANUCLEUS_IMPL + "javax.jdo.option.ConnectionDriverName", "org.hsqldb.jdbcDriver");
map.put(ISIS_PERSISTOR_DATANUCLEUS_IMPL + "javax.jdo.option.ConnectionUserName", "sa");
map.put(ISIS_PERSISTOR_DATANUCLEUS_IMPL + "javax.jdo.option.ConnectionPassword", "");
return map;
}
public static Map<String,String> withDataNucleusProperties(final Map<String, String> map) {
// Don't do validations that consume setup time.
map.put(ISIS_PERSISTOR_DATANUCLEUS_IMPL + "datanucleus.schema.autoCreateAll", "true");
map.put(ISIS_PERSISTOR_DATANUCLEUS_IMPL + "datanucleus.schema.validateAll", "false");
// other properties as per WEB-INF/persistor_datanucleus.properties
map.put(ISIS_PERSISTOR_DATANUCLEUS_IMPL + "datanucleus.persistenceByReachabilityAtCommit", "false");
map.put(ISIS_PERSISTOR_DATANUCLEUS_IMPL + "datanucleus.identifier.case", "MixedCase");
map.put(ISIS_PERSISTOR_DATANUCLEUS_IMPL + "datanucleus.cache.level2.type" ,"none");
map.put(ISIS_PERSISTOR_DATANUCLEUS_IMPL + "datanucleus.cache.level2.mode", "ENABLE_SELECTIVE");
return map;
}
public static Map<String,String> withIsisIntegTestProperties(final Map<String, String> map) {
// automatically install any fixtures that might have been registered
map.put(ISIS_PERSISTOR_DATANUCLEUS + "install-fixtures", "true");
map.put(ISIS_PERSISTOR + "enforceSafeSemantics", "false");
map.put("isis.deploymentType", "server_prototype");
map.put("isis.services.eventbus.allowLateRegistration", "true");
return map;
}
}
}